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
407 lines
16 KiB
Swift
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)
|
|
}
|
|
}
|