// // ContentView.swift // Shared // // Created by Trey Tartt on 1/5/22. // import SwiftUI import CoreData import Charts 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 @State private var showingSheet = false @State private var showTodayInput = true @State private var selectedEntry: MoodEntry? @State private var showUpdateEntryAlert = false @State private var headerHeight: CGFloat = 150 let minHeaderHeight = 88.0 let maxHeaderHeight = 150.0 @State private var headerOpacity: Double = 1.0 @ObservedObject var viewModel = ContentModeViewModel() init(){ UITabBar.appearance().backgroundColor = UIColor.systemBackground } 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") } GraphView() .tabItem { Label(String(localized: "content_view_tab_stats"), systemImage: "chart.line.uptrend.xyaxis") } }.sheet(isPresented: $needsOnboarding, onDismiss: { }, content: { OnboardingMain(onboardingData: viewModel.savedOnboardingData, updateBoardingDataClosure: { onboardingData in needsOnboarding = false viewModel.updateOnboardingData(onboardingData: onboardingData) }) }).alert(String(localized: "content_view_fill_in_missing_entry"), isPresented: $showUpdateEntryAlert) { ForEach(Mood.allValues) { mood in Button(mood.strValue, action: { if let selectedMissingEntry = selectedEntry { viewModel.update(entry: selectedMissingEntry, toMood: mood) } showUpdateEntryAlert = false }) } if deleteEnabled { Button(String(localized: "content_view_delete_entry"), action: { if let selectedMissingEntry = selectedEntry { viewModel.update(entry: selectedMissingEntry, toMood: Mood.missing) } showUpdateEntryAlert = false }) } Button(String(localized: "content_view_fill_in_missing_entry_cancel"), role: .cancel, action: { selectedEntry = nil showUpdateEntryAlert = false }) } } 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 func weekdayName(fromDate date: Date) -> String { let weekday = Calendar.current.component(.weekday, from: date) let calendar = Calendar.current let dayIndex = ((weekday - 1) + (calendar.firstWeekday - 1)) % 7 return calendar.weekdaySymbols[dayIndex] } private func monthName(fromMonthInt: Int) -> String { let monthName = DateFormatter().monthSymbols[fromMonthInt-1] return monthName } private func dayFormat(fromDate date: Date) -> String { let components = Calendar.current.dateComponents([.day], from: date) let day = components.day! let formatter = NumberFormatter() formatter.numberStyle = .ordinal return formatter.string(from: NSNumber(integerLiteral: day)) ?? "" } func calcuateViewAlpha() { let perc = (((Double(headerHeight) - minHeaderHeight) * 100) / (maxHeaderHeight - minHeaderHeight)) / 100 headerOpacity = perc } func calculateHeight(minHeight: CGFloat, maxHeight: CGFloat, yOffset: CGFloat) { calcuateViewAlpha() let newValue = maxHeight + yOffset // 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 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(UIColor.systemBackground) ) .coordinateSpace(name: "scroll") .onPreferenceChange(ViewOffsetKey.self) { value in calculateHeight(minHeight: 88, maxHeight: 180, yOffset: value) } } private func SectionHeaderView(month: Int, year: Int) -> some View { Text("\(monthName(fromMonthInt: month)) \(String(year))") .font(.title) .foregroundColor(Color(UIColor.label)) .frame(maxWidth: .infinity, alignment: .leading) .padding() .background( Color(UIColor.systemBackground) ) } 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) VStack { HStack { Text(weekdayName(fromDate:entry.forDate!)) .font(.title3) .foregroundColor(Color(UIColor.label)) Text(" - ") .padding([.leading, .trailing], -10) Text(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) } } } private var headerView: some View { VStack { if viewModel.shouldShowVotingHeader() { AddMoodHeaderView(addItemHeaderClosure: { (mood, date) in withAnimation { viewModel.add(mood: mood, forDate: date) } }) .frame(height: headerHeight) .frame(minWidth: 0, maxWidth: .infinity) } else { HeaderStatsView(fakeData: false, backDays: 30) .frame(height: headerHeight) // should match backDays above Text(String(format: String(localized: "content_view_header_title"), 30)) .font(.body) .foregroundColor(Color(UIColor.systemGray)) .frame(maxWidth: .infinity, alignment: .center) } } } private var emptyView: some View { ZStack { Color(UIColor.systemBackground) VStack { Text(String(localized: "content_view_empty_title")) .font(.title) .foregroundColor(Color(UIColor.label)) .padding() Text(String(localized: "content_view_empty_title")) .font(.body) .foregroundColor(Color(UIColor.label)) .padding() AddMoodHeaderView(addItemHeaderClosure: { (mood, date) in withAnimation { viewModel.add(mood: mood, forDate: date) } }, overrideDay: viewModel.shouldShowVotingHeader() ? .Today : .Previous) } } .fixedSize(horizontal: false, vertical: true) .clipShape(RoundedRectangle(cornerRadius: 25, style: .continuous)) .padding() } private var mainView: some View { VStack { settingsButtonView if viewModel.hasNoData { Spacer() emptyView Spacer() } else { ZStack { VStack { headerView Spacer() } .opacity(headerOpacity) VStack { SmallRollUpHeaderView(fakeData: false, backDays: 30) .padding([.leading, .trailing]) Spacer() } .opacity(1 - headerOpacity) } .frame(height: headerHeight + 20) listView .padding([.leading, .trailing]) } }.background( BGView().equatable() ) } } 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) } }