Files
Reflect/Shared/views/ContentView.swift
Trey t 675e44bca9 fix issue with two votes on the same date
fix issue with header not showing correct vote date
split logic for Persistence into different files
create class that deals with voting time, existing votes, and what should be shown based on that
2022-02-17 14:46:11 -06:00

407 lines
16 KiB
Swift

//
// ContentView.swift
// Shared
//
// Created by Trey Tartt on 1/5/22.
//
import SwiftUI
import CoreData
import Charts
struct ContentViewConstants {
static let maxHeaderHeight = 200.0
static let minHeaderHeight = 120.0
}
struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext
@AppStorage(UserDefaultsStore.Keys.needsOnboarding.rawValue, store: GroupUserDefaults.groupDefaults) private var needsOnboarding = true
@AppStorage(UserDefaultsStore.Keys.deleteEnable.rawValue, store: GroupUserDefaults.groupDefaults) private var deleteEnabled = true
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
// MARK: top header storage
@AppStorage(UserDefaultsStore.Keys.contentViewCurrentSelectedHeaderViewBackDays.rawValue, store: GroupUserDefaults.groupDefaults) private var currentSelectedHeaderViewBackDays: Int = 30
@AppStorage(UserDefaultsStore.Keys.contentViewHeaderTagViewOneViewType.rawValue, store: GroupUserDefaults.groupDefaults) private var firstSwichableHeaderViewType: MainSwitchableViewType = .total
@AppStorage(UserDefaultsStore.Keys.contentViewHeaderTagViewTwoViewType.rawValue, store: GroupUserDefaults.groupDefaults) private var secondSwichableHeaderViewType: MainSwitchableViewType = .total
@AppStorage(UserDefaultsStore.Keys.contentViewHeaderTag.rawValue, store: GroupUserDefaults.groupDefaults) private var switchableViewSelectedIndex = 1
//
// MARK: edit row properties
@State private var showingSheet = false
@State private var selectedEntry: MoodEntry?
//
// MARK: ?? properties
@State private var showTodayInput = true
@State private var showUpdateEntryAlert = false
// MARK: header properties
@State private var headerHeight: CGFloat = ContentViewConstants.maxHeaderHeight
@State private var headerViewType: MainSwitchableViewType = .total
@State private var currentSelectedHeaderViewViewType: MainSwitchableViewType = .total
@State private var headerOpacity: Double = 1.0
//
@ObservedObject var viewModel = ContentModeViewModel()
init(){
UIPageControl.appearance().currentPageIndicatorTintColor = UIColor.label
UIPageControl.appearance().pageIndicatorTintColor = UIColor.systemGray
UITabBar.appearance().backgroundColor = UIColor.secondarySystemBackground
}
var body: some View {
TabView {
mainView
.tabItem {
Label(String(localized: "content_view_tab_main"), systemImage: "list.dash")
}
FilterView()
.tabItem {
Label(String(localized: "content_view_tab_filter"), systemImage: "calendar.circle")
}
SharingListView()
.tabItem {
Label(String(localized: "content_view_tab_share"), systemImage: "square.and.arrow.up")
}
}.sheet(isPresented: $needsOnboarding, onDismiss: {
}, content: {
OnboardingMain(onboardingData: viewModel.savedOnboardingData,
updateBoardingDataClosure: { onboardingData in
needsOnboarding = false
viewModel.updateOnboardingData(onboardingData: onboardingData)
})
}).alert(updateTitleHeader(forEntry: selectedEntry),
isPresented: $showUpdateEntryAlert) {
ForEach(Mood.allValues) { mood in
Button(mood.strValue, action: {
if let selectedEntry = selectedEntry {
viewModel.update(entry: selectedEntry, toMood: mood)
}
showUpdateEntryAlert = false
selectedEntry = nil
})
}
if let selectedEntry = selectedEntry,
deleteEnabled{
Button(String(localized: "content_view_delete_entry"), action: {
viewModel.update(entry: selectedEntry, toMood: Mood.missing)
showUpdateEntryAlert = false
})
}
Button(String(localized: "content_view_fill_in_missing_entry_cancel"), role: .cancel, action: {
selectedEntry = nil
showUpdateEntryAlert = false
})
}
}
// MARK: functions that do view type work
func calcuateViewAlpha() {
let perc = (((Double(headerHeight) - ContentViewConstants.minHeaderHeight) * 100) / (ContentViewConstants.maxHeaderHeight - ContentViewConstants.minHeaderHeight)) / 100
headerOpacity = perc
}
func calculateHeight(minHeight: CGFloat, maxHeight: CGFloat, yOffset: CGFloat) {
let newValue = maxHeight + yOffset
calcuateViewAlpha()
// If scrolling up, yOffset will be a negative number
if newValue < minHeight {
// SCROLLING UP
// Never go smaller than our minimum height
headerHeight = minHeight
return
}
if newValue > maxHeight {
// SCROLLING UP
// Never go smaller than our minimum height
headerHeight = maxHeight
return
}
// SCROLLING DOWN
headerHeight = newValue
}
private func updateTitleHeader(forEntry entry: MoodEntry?) -> String {
guard let entry = entry else {
return ""
}
guard let forDate = entry.forDate else {
return ""
}
let components = Calendar.current.dateComponents([.day, .month, .year], from: forDate)
// let day = components.day!
let month = components.month!
let year = components.year!
let monthName = Random.monthName(fromMonthInt: month)
let weekday = Random.weekdayName(fromDate:entry.forDate!)
let dayz = Random.dayFormat(fromDate:entry.forDate!)
let string = weekday + " " + monthName + " " + dayz + " " + String(year)
return String(format: String(localized: "content_view_fill_in_missing_entry"), string)
}
// MARK: Views
private var settingsButtonView: some View {
HStack {
Spacer()
Button(action: {
showingSheet.toggle()
}, label: {
Image(systemName: "gear")
.foregroundColor(Color(UIColor.darkGray))
.font(.system(size: 20))
}).sheet(isPresented: $showingSheet) {
SettingsView(editedDataClosure: {
withAnimation{
viewModel.updateData()
}
}, updateBoardingDataClosure: { onboardingData in
viewModel.updateOnboardingData(onboardingData: onboardingData)
})
}.padding(.trailing)
}
}
private var headerView: some View {
VStack {
if ShowBasedOnVoteLogics.isMissingCurrentVote() {
AddMoodHeaderView(addItemHeaderClosure: { (mood, date) in
withAnimation {
viewModel.add(mood: mood, forDate: date, entryType: .header)
}
})
.frame(height: headerHeight)
.frame(minWidth: 0, maxWidth: .infinity)
} else {
// selection hre doesn't work ...
TabView(selection: $switchableViewSelectedIndex) {
SwitchableView(daysBack: 30,
viewType: $firstSwichableHeaderViewType,
headerTypeChanged: { viewType in
firstSwichableHeaderViewType = viewType
currentSelectedHeaderViewViewType = firstSwichableHeaderViewType
})
.tag(1)
.frame(height: headerHeight)
.frame(minWidth: 0, maxWidth: .infinity)
.contentShape(Rectangle())
SwitchableView(daysBack: 7,
viewType: $secondSwichableHeaderViewType,
headerTypeChanged: { viewType in
secondSwichableHeaderViewType = viewType
currentSelectedHeaderViewViewType = secondSwichableHeaderViewType
})
.tag(2)
.frame(height: headerHeight)
.frame(minWidth: 0, maxWidth: .infinity)
.contentShape(Rectangle())
}
.tabViewStyle(.page)
.onChange(of: switchableViewSelectedIndex) { value in
if value == 1 {
currentSelectedHeaderViewBackDays = 30
currentSelectedHeaderViewViewType = firstSwichableHeaderViewType
}
if value == 2 {
currentSelectedHeaderViewBackDays = 7
currentSelectedHeaderViewViewType = secondSwichableHeaderViewType
}
}
}
}
}
private var listView: some View {
ScrollView {
LazyVStack(spacing: 5, pinnedViews: [.sectionHeaders]) {
ForEach(viewModel.grouped.sorted(by: {
$0.key > $1.key
}), id: \.key) { year, months in
// for reach month
ForEach(months.sorted(by: {
$0.key > $1.key
}), id: \.key) { month, entries in
Section(header: SectionHeaderView(month: month, year: year)) {
monthListView(month: month, year: year, entries: entries)
}
}
}
}.background(
GeometryReader { proxy in
let offset = proxy.frame(in: .named("scroll")).minY
Color.clear.preference(key: ViewOffsetKey.self, value: offset)
}
)
}
.background(
Color(theme.currentTheme.secondaryBGColor)
)
.coordinateSpace(name: "scroll")
.onPreferenceChange(ViewOffsetKey.self) { value in
if viewModel.numberOfItems > 10 {
calculateHeight(minHeight: ContentViewConstants.minHeaderHeight,
maxHeight: ContentViewConstants.maxHeaderHeight,
yOffset: value)
}
}
.cornerRadius(10, corners: [.topLeft, .topRight])
}
private var mainView: some View {
VStack {
settingsButtonView
if viewModel.hasNoData {
Spacer()
EmptyContentView(viewModel: viewModel)
Spacer()
} else {
ZStack {
VStack {
headerView
Spacer()
}
.opacity(headerOpacity)
VStack {
SmallRollUpHeaderView(fakeData: false,
backDays: $currentSelectedHeaderViewBackDays,
viewType: $currentSelectedHeaderViewViewType)
.background(
Color(theme.currentTheme.secondaryBGColor)
)
.cornerRadius(10, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
.padding([.top, .bottom], 5)
Spacer()
}
.opacity(1 - headerOpacity)
}
.frame(height: headerHeight + 20)
listView
.padding(.top, -25)
}
}
.padding()
.padding(.bottom, 5)
.background(
theme.currentTheme.bg
.edgesIgnoringSafeArea(.all)
)
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
PersistenceController.shared.fillInMissingDates()
viewModel.updateData()
}
}
}
// view that make up the list body
extension ContentView {
private func SectionHeaderView(month: Int, year: Int) -> some View {
Text("\(Random.monthName(fromMonthInt: month)) \(String(year))")
.font(.title)
.foregroundColor(Color(UIColor.label))
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(
Color(theme.currentTheme.secondaryBGColor)
)
}
private func monthListView(month: Int, year: Int, entries: [MoodEntry]) -> some View {
VStack {
// for reach all entries
ForEach(entries.sorted(by: {
return $0.forDate! > $1.forDate!
}), id: \.self) { entry in
entryListView(entry: entry)
.contentShape(Rectangle())
.onTapGesture(perform: {
selectedEntry = entry
showUpdateEntryAlert = true
})
}
}
}
private func entryListView(entry: MoodEntry) -> some View {
HStack {
entry.mood.icon
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 40, height: 40, alignment: .center)
.foregroundColor(entry.mood.color)
.padding(.leading, 5)
VStack {
HStack {
Text(Random.weekdayName(fromDate:entry.forDate!))
.font(.title3)
.foregroundColor(Color(UIColor.label))
Text(" - ")
.padding([.leading, .trailing], -10)
Text(Random.dayFormat(fromDate:entry.forDate!))
.font(.title3)
.foregroundColor(Color(UIColor.label))
Spacer()
}
.multilineTextAlignment(.leading)
Text(entry.moodValue == Mood.missing.rawValue ? String(localized: "mood_value_missing_tap_to_add") : "\(entry.moodString)")
.font(.body)
.foregroundColor(Color(UIColor.systemGray))
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
}
struct ViewOffsetKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
return formatter
}()
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environment(\.managedObjectContext, PersistenceController.shared.viewContext)
.onAppear(perform: {
PersistenceController.shared.populateMemory()
})
ContentView()
.preferredColorScheme(.dark)
.environment(\.managedObjectContext, PersistenceController.shared.viewContext)
}
}