From 675e44bca9637bbf1c74216ac034878b77c4b5d5 Mon Sep 17 00:00:00 2001 From: Trey t Date: Thu, 17 Feb 2022 14:46:11 -0600 Subject: [PATCH] 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 --- Feels.xcodeproj/project.pbxproj | 40 ++- Shared/FeelsApp.swift | 2 + Shared/Models/ContentModeViewModel.swift | 41 +-- Shared/Persisence/Persistence.swift | 104 +++++++ Shared/Persisence/PersistenceADD.swift | 63 ++++ Shared/Persisence/PersistenceDELETE.swift | 29 ++ Shared/Persisence/PersistenceGET.swift | 92 ++++++ Shared/Persisence/PersistenceHelper.swift | 85 ++++++ Shared/Persistence.swift | 340 ---------------------- Shared/ShowBasedOnVoteLogics.swift | 123 ++++++++ Shared/views/AddMoodHeaderView.swift | 44 +-- Shared/views/ContentView.swift | 11 +- 12 files changed, 547 insertions(+), 427 deletions(-) create mode 100644 Shared/Persisence/Persistence.swift create mode 100644 Shared/Persisence/PersistenceADD.swift create mode 100644 Shared/Persisence/PersistenceDELETE.swift create mode 100644 Shared/Persisence/PersistenceGET.swift create mode 100644 Shared/Persisence/PersistenceHelper.swift delete mode 100644 Shared/Persistence.swift create mode 100644 Shared/ShowBasedOnVoteLogics.swift diff --git a/Feels.xcodeproj/project.pbxproj b/Feels.xcodeproj/project.pbxproj index 924266a..b8ab70d 100644 --- a/Feels.xcodeproj/project.pbxproj +++ b/Feels.xcodeproj/project.pbxproj @@ -27,6 +27,16 @@ 1C358FBE27B4D1F2002C83A6 /* CurrentStreakTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C358FBD27B4D1F2002C83A6 /* CurrentStreakTemplate.swift */; }; 1C358FC027B4D20C002C83A6 /* MonthTotalTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C358FBF27B4D20C002C83A6 /* MonthTotalTemplate.swift */; }; 1C358FC227B4D227002C83A6 /* WeekTotalTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C358FC127B4D227002C83A6 /* WeekTotalTemplate.swift */; }; + 1C4FF3BB27BEDDF000BE8F34 /* ShowBasedOnVoteLogics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C4FF3BA27BEDDF000BE8F34 /* ShowBasedOnVoteLogics.swift */; }; + 1C4FF3BC27BEDF6600BE8F34 /* ShowBasedOnVoteLogics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C4FF3BA27BEDDF000BE8F34 /* ShowBasedOnVoteLogics.swift */; }; + 1C4FF3BE27BEDF9100BE8F34 /* PersistenceHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C4FF3BD27BEDF9100BE8F34 /* PersistenceHelper.swift */; }; + 1C4FF3C027BEE06900BE8F34 /* PersistenceGET.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C4FF3BF27BEE06900BE8F34 /* PersistenceGET.swift */; }; + 1C4FF3C127BEE06900BE8F34 /* PersistenceGET.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C4FF3BF27BEE06900BE8F34 /* PersistenceGET.swift */; }; + 1C4FF3C327BEE07200BE8F34 /* PersistenceDELETE.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C4FF3C227BEE07200BE8F34 /* PersistenceDELETE.swift */; }; + 1C4FF3C427BEE07200BE8F34 /* PersistenceDELETE.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C4FF3C227BEE07200BE8F34 /* PersistenceDELETE.swift */; }; + 1C4FF3C727BEE09E00BE8F34 /* PersistenceADD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C4FF3C627BEE09E00BE8F34 /* PersistenceADD.swift */; }; + 1C4FF3C827BEE09E00BE8F34 /* PersistenceADD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C4FF3C627BEE09E00BE8F34 /* PersistenceADD.swift */; }; + 1C4FF3C927BEE0C300BE8F34 /* PersistenceHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C4FF3BD27BEDF9100BE8F34 /* PersistenceHelper.swift */; }; 1C5F4976279C84090092F1B4 /* OnboardingData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C5F4975279C84090092F1B4 /* OnboardingData.swift */; }; 1C5F4978279C945E0092F1B4 /* UserDefaultsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C5F4977279C945E0092F1B4 /* UserDefaultsStore.swift */; }; 1C683FCA2792281400745862 /* Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C683FC92792281400745862 /* Stats.swift */; }; @@ -149,6 +159,11 @@ 1C358FBD27B4D1F2002C83A6 /* CurrentStreakTemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentStreakTemplate.swift; sourceTree = ""; }; 1C358FBF27B4D20C002C83A6 /* MonthTotalTemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonthTotalTemplate.swift; sourceTree = ""; }; 1C358FC127B4D227002C83A6 /* WeekTotalTemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeekTotalTemplate.swift; sourceTree = ""; }; + 1C4FF3BA27BEDDF000BE8F34 /* ShowBasedOnVoteLogics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowBasedOnVoteLogics.swift; sourceTree = ""; }; + 1C4FF3BD27BEDF9100BE8F34 /* PersistenceHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceHelper.swift; sourceTree = ""; }; + 1C4FF3BF27BEE06900BE8F34 /* PersistenceGET.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceGET.swift; sourceTree = ""; }; + 1C4FF3C227BEE07200BE8F34 /* PersistenceDELETE.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceDELETE.swift; sourceTree = ""; }; + 1C4FF3C627BEE09E00BE8F34 /* PersistenceADD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceADD.swift; sourceTree = ""; }; 1C5F4975279C84090092F1B4 /* OnboardingData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingData.swift; sourceTree = ""; }; 1C5F4977279C945E0092F1B4 /* UserDefaultsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsStore.swift; sourceTree = ""; }; 1C683FC92792281400745862 /* Stats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stats.swift; sourceTree = ""; }; @@ -275,6 +290,18 @@ path = SharingTemplates; sourceTree = ""; }; + 1C4FF3C527BEE07800BE8F34 /* Persisence */ = { + isa = PBXGroup; + children = ( + 1CD90AEF278C7DDF001C4FEA /* Persistence.swift */, + 1C4FF3C627BEE09E00BE8F34 /* PersistenceADD.swift */, + 1C4FF3C227BEE07200BE8F34 /* PersistenceDELETE.swift */, + 1C4FF3BF27BEE06900BE8F34 /* PersistenceGET.swift */, + 1C4FF3BD27BEDF9100BE8F34 /* PersistenceHelper.swift */, + ); + path = Persisence; + sourceTree = ""; + }; 1CA03771279A291F00D26164 /* Onboarding */ = { isa = PBXGroup; children = ( @@ -345,9 +372,10 @@ 1C744F2B278CE15600953A57 /* AppDelegate.swift */, 1CC469A9278F30A0003E0C6E /* BGTask.swift */, 1CD90B75278C8119001C4FEA /* LocalNotification.swift */, - 1CD90AEF278C7DDF001C4FEA /* Persistence.swift */, + 1C4FF3C527BEE07800BE8F34 /* Persisence */, 1CD90B5C278C7EAD001C4FEA /* Random.swift */, 1C683FC92792281400745862 /* Stats.swift */, + 1C4FF3BA27BEDDF000BE8F34 /* ShowBasedOnVoteLogics.swift */, 1CA03771279A291F00D26164 /* Onboarding */, 1C26190127960CDA00FDC148 /* Protocols */, 1CAD602A27A5C1C800C520BD /* Views */, @@ -639,6 +667,7 @@ 1CAD603C27A5C1C800C520BD /* HeaderStatsView.swift in Sources */, 1CAD603827A5C1C800C520BD /* AddMoodHeaderView.swift in Sources */, 1CA0377C279B605000D26164 /* OnboardingWrapup.swift in Sources */, + 1C4FF3C327BEE07200BE8F34 /* PersistenceDELETE.swift in Sources */, 1CA03775279A294800D26164 /* OnboardingDay.swift in Sources */, 1CAD603727A5C1C800C520BD /* FilterView.swift in Sources */, 1C683FCA2792281400745862 /* Stats.swift in Sources */, @@ -646,6 +675,7 @@ 1CD90B76278C8119001C4FEA /* LocalNotification.swift in Sources */, 1C358FB627B0AE15002C83A6 /* AllMoodsTotalTemplate.swift in Sources */, 1CD90B16278C7DE0001C4FEA /* Feels.xcdatamodeld in Sources */, + 1C4FF3BE27BEDF9100BE8F34 /* PersistenceHelper.swift in Sources */, 1CC469AA278F30A0003E0C6E /* BGTask.swift in Sources */, 1CAD603B27A5C1C800C520BD /* ContentView.swift in Sources */, 1C5F4976279C84090092F1B4 /* OnboardingData.swift in Sources */, @@ -658,6 +688,8 @@ 1C744F2C278CE15600953A57 /* AppDelegate.swift in Sources */, 1CD90B63278C7EBA001C4FEA /* Mood.swift in Sources */, 1C358FBE27B4D1F2002C83A6 /* CurrentStreakTemplate.swift in Sources */, + 1C4FF3C727BEE09E00BE8F34 /* PersistenceADD.swift in Sources */, + 1C4FF3BB27BEDDF000BE8F34 /* ShowBasedOnVoteLogics.swift in Sources */, 1CAD603527A5C1C800C520BD /* SettingsView.swift in Sources */, 1CD90B53278C7E7A001C4FEA /* FeelsWidget.intentdefinition in Sources */, 1CC469AC27907D48003E0C6E /* DayChartView.swift in Sources */, @@ -677,6 +709,7 @@ 1CD90B18278C7DE0001C4FEA /* FeelsApp.swift in Sources */, 1C358FC027B4D20C002C83A6 /* MonthTotalTemplate.swift in Sources */, 1CA03777279A295600D26164 /* OnboardingTitle.swift in Sources */, + 1C4FF3C027BEE06900BE8F34 /* PersistenceGET.swift in Sources */, 1CEC967127B9C2BB00CC8688 /* CustomIcon.swift in Sources */, 1C358FAD27ADD0C3002C83A6 /* Theme.swift in Sources */, 1C02589C27B9677A00EB91AC /* CreateIconView.swift in Sources */, @@ -722,6 +755,8 @@ files = ( 1CD90B65278C7EBA001C4FEA /* Mood.swift in Sources */, 1CEC967227B9C9FB00CC8688 /* IconView.swift in Sources */, + 1C4FF3BC27BEDF6600BE8F34 /* ShowBasedOnVoteLogics.swift in Sources */, + 1C4FF3C927BEE0C300BE8F34 /* PersistenceHelper.swift in Sources */, 1CA2662D2793908700C0E12C /* Persistence.swift in Sources */, 1CD90B5F278C7EAD001C4FEA /* Random.swift in Sources */, 1CD90B68278C7EBA001C4FEA /* MoodEntryExtension.swift in Sources */, @@ -731,6 +766,9 @@ 1C683FCB2792281400745862 /* Stats.swift in Sources */, 1CEC967327B9CA0C00CC8688 /* CustomIcon.swift in Sources */, 1C10E25027A1AB220047948B /* OnboardingDay.swift in Sources */, + 1C4FF3C427BEE07200BE8F34 /* PersistenceDELETE.swift in Sources */, + 1C4FF3C827BEE09E00BE8F34 /* PersistenceADD.swift in Sources */, + 1C4FF3C127BEE06900BE8F34 /* PersistenceGET.swift in Sources */, 1C10E24F27A1AB1D0047948B /* OnboardingData.swift in Sources */, 1CD90B52278C7E7A001C4FEA /* FeelsWidget.intentdefinition in Sources */, 1CD90B4D278C7E7A001C4FEA /* FeelsWidget.swift in Sources */, diff --git a/Shared/FeelsApp.swift b/Shared/FeelsApp.swift index 0e1c5d9..ec240e2 100644 --- a/Shared/FeelsApp.swift +++ b/Shared/FeelsApp.swift @@ -36,6 +36,8 @@ struct FeelsApp: App { if phase == .active { UIApplication.shared.applicationIconBadgeNumber = 0 + print("UserDefaultsStore input day", UserDefaultsStore.getOnboarding().inputDay) + print("UserDefaultsStore input date", UserDefaultsStore.getOnboarding().date) } } } diff --git a/Shared/Models/ContentModeViewModel.swift b/Shared/Models/ContentModeViewModel.swift index 05c3a77..26fe3f5 100644 --- a/Shared/Models/ContentModeViewModel.swift +++ b/Shared/Models/ContentModeViewModel.swift @@ -47,51 +47,12 @@ class ContentModeViewModel: ObservableObject { grouped = PersistenceController.shared.splitIntoYearMonth() numberOfItems = numberOfEntries } - - public func shouldShowVotingHeader() -> Bool { - if isMissingCurrentVote() { - return true - } - - return savedOnboardingData.ableToVoteBasedOnCurentTime() ? true : false - } - + public func updateOnboardingData(onboardingData: OnboardingData) { self.savedOnboardingData = UserDefaultsStore.saveOnboarding(onboardingData: onboardingData) LocalNotification.scheduleReminder(atTime: onboardingData.date, withTitle: onboardingData.title) } - - private func isMissingCurrentVote() -> Bool { - let latestVoteUnLocked = UserDefaultsStore.getOnboarding().ableToVoteBasedOnCurentTime() - let inputDay = UserDefaultsStore.getOnboarding().inputDay - - var startDate: Date? - - switch (latestVoteUnLocked, inputDay) { - case (true, .Previous): - startDate = Calendar.current.date(byAdding: .day, value: -1, to: Date())! - case (true, .Today): - startDate = Date() - case (false, .Previous): - startDate = Calendar.current.date(byAdding: .day, value: -2, to: Date())! - case (false, .Today): - startDate = Calendar.current.date(byAdding: .day, value: -1, to: Date())! - } - - startDate = Calendar.current.startOfDay(for: startDate!) - let endDate = Calendar.current.date(byAdding: .day, value: 1, to: startDate!)! - let fetchRequest = NSFetchRequest(entityName: "MoodEntry") - let fromPredicate = NSPredicate(format: "%@ <= %K", startDate! - as NSDate, #keyPath(MoodEntry.forDate)) - let toPredicate = NSPredicate(format: "%K < %@", #keyPath(MoodEntry.forDate), endDate as NSDate) - let datePredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [fromPredicate, toPredicate]) - fetchRequest.predicate = datePredicate - let entries = try! PersistenceController.shared.viewContext.count(for: fetchRequest) - - return entries == 0 - } - public func updateData() { getGroupedData() } diff --git a/Shared/Persisence/Persistence.swift b/Shared/Persisence/Persistence.swift new file mode 100644 index 0000000..f58ebb0 --- /dev/null +++ b/Shared/Persisence/Persistence.swift @@ -0,0 +1,104 @@ +// +// Persistence.swift +// Shared +// +// Created by Trey Tartt on 1/5/22. +// + +import CoreData +import SwiftUI + +class PersistenceController { + @AppStorage(UserDefaultsStore.Keys.useCloudKit.rawValue, store: GroupUserDefaults.groupDefaults) private var useCloudKit = false + + static let shared = PersistenceController.persistenceController + + private static var persistenceController: PersistenceController { + return PersistenceController(inMemory: false) + } + + public var viewContext: NSManagedObjectContext { + return PersistenceController.shared.container.viewContext + } + + public var switchContainerListeners = [(() -> Void)]() + + public var earliestEntry: MoodEntry? { + let fetchRequest = NSFetchRequest(entityName: "MoodEntry") + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "forDate", ascending: true)] + let first = try! viewContext.fetch(fetchRequest).first + return first ?? nil + } + + public var latestEntry: MoodEntry? { + let fetchRequest = NSFetchRequest(entityName: "MoodEntry") + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "forDate", ascending: true)] + let last = try! viewContext.fetch(fetchRequest).last + return last ?? nil + } + + lazy var container: NSPersistentContainer = { + setupContainer() + }() + + func switchContainer() { + try? viewContext.save() + container = setupContainer() + for item in switchContainerListeners { + item() + } + } + + private func setupContainer() -> NSPersistentContainer { + if useCloudKit { + container = NSPersistentCloudKitContainer(name: "Feels") + } else { + container = NSCustomPersistentContainer(name: "Feels") + } + + for description in container.persistentStoreDescriptions { + description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey) + description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey) + description.setOption(true as NSNumber, forKey: NSMigratePersistentStoresAutomaticallyOption) + description.setOption(true as NSNumber, forKey: NSInferMappingModelAutomaticallyOption) + } + + container.loadPersistentStores(completionHandler: { (storeDescription, error) in + self.container.viewContext.automaticallyMergesChangesFromParent = true + + if let error = error as NSError? { + fatalError("Unresolved error \(error), \(error.userInfo)") + } + }) + + return container + } + + init(inMemory: Bool = false) { + container = setupContainer() + } +} + +extension NSManagedObjectContext { + /// Executes the given `NSBatchDeleteRequest` and directly merges the changes to bring the given managed object context up to date. + /// + /// - Parameter batchDeleteRequest: The `NSBatchDeleteRequest` to execute. + /// - Throws: An error if anything went wrong executing the batch deletion. + public func executeAndMergeChanges(using batchDeleteRequest: NSBatchDeleteRequest) throws { + batchDeleteRequest.resultType = .resultTypeObjectIDs + let result = try execute(batchDeleteRequest) as? NSBatchDeleteResult + let changes: [AnyHashable: Any] = [NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []] + NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [self]) + } +} + +class NSCustomPersistentContainer: NSPersistentContainer { + override open class func defaultDirectoryURL() -> URL { + var storeURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Constants.groupShareId) + storeURL = storeURL?.appendingPathComponent("Feels.sqlite") +#if DEBUG + storeURL = storeURL?.appendingPathComponent("Feels-Debug.sqlite") +#endif + return storeURL! + } +} diff --git a/Shared/Persisence/PersistenceADD.swift b/Shared/Persisence/PersistenceADD.swift new file mode 100644 index 0000000..f996a67 --- /dev/null +++ b/Shared/Persisence/PersistenceADD.swift @@ -0,0 +1,63 @@ +// +// PersistenceADD.swift +// Feels +// +// Created by Trey Tartt on 2/17/22. +// + +import CoreData + +extension PersistenceController { + public func add(mood: Mood, forDate date: Date, entryType: EntryType) { + if let existingEntry = getEntry(byDate: date) { + viewContext.delete(existingEntry) + try? viewContext.save() + } + + let newItem = MoodEntry(context: viewContext) + newItem.timestamp = Date() + newItem.moodValue = Int16(mood.rawValue) + newItem.forDate = date + newItem.weekDay = Int16(Calendar.current.component(.weekday, from: date)) + newItem.canEdit = true + newItem.canDelete = true + newItem.entryType = Int16(entryType.rawValue) + + do { + try viewContext.save() + } catch { + let nsError = error as NSError + fatalError("Unresolved error \(nsError), \(nsError.userInfo)") + } + } + + func fillInMissingDates() { + let endDate = ShowBasedOnVoteLogics.getLastDateVoteShouldExist() + + let fetchRequest = NSFetchRequest(entityName: "MoodEntry") + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "forDate", ascending: false)] + let entries = try! viewContext.fetch(fetchRequest) + + if let firstEntry = entries.last?.forDate { + let allDates: [Date] = Date.dates(from: firstEntry, to: endDate).map({ + let zeroDate = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: $0)! + return zeroDate + }) + + let existingEntries: [Date] = entries.compactMap({ + if let date = $0.forDate { + let zeroDate = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: date)! + return zeroDate + } + return nil + }) + + let allDatesSet = Set(allDates) + let existingEntriesSet = Set(existingEntries) + let missing = Array(allDatesSet.subtracting(existingEntriesSet)).sorted(by: >) + for date in missing { + add(mood: .missing, forDate: date, entryType: .listView) + } + } + } +} diff --git a/Shared/Persisence/PersistenceDELETE.swift b/Shared/Persisence/PersistenceDELETE.swift new file mode 100644 index 0000000..0ddd016 --- /dev/null +++ b/Shared/Persisence/PersistenceDELETE.swift @@ -0,0 +1,29 @@ +// +// PersistenceDELETE.swift +// Feels +// +// Created by Trey Tartt on 2/17/22. +// + +import CoreData + +extension PersistenceController { + func clearDB() { + let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "MoodEntry") + let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) + + do { + try viewContext.executeAndMergeChanges(using: deleteRequest) + try viewContext.save() + } catch let error as NSError { + fatalError("Unresolved error \(error), \(error.userInfo)") + } + } + + func delete(forDate: Date) { + if let entry = PersistenceController.shared.getEntry(byDate: forDate) { + viewContext.delete(entry) + try! viewContext.save() + } + } +} diff --git a/Shared/Persisence/PersistenceGET.swift b/Shared/Persisence/PersistenceGET.swift new file mode 100644 index 0000000..7422283 --- /dev/null +++ b/Shared/Persisence/PersistenceGET.swift @@ -0,0 +1,92 @@ +// +// PersistenceGET.swift +// Feels +// +// Created by Trey Tartt on 2/17/22. +// + +import CoreData + +extension PersistenceController { + public func getEntry(byDate date: Date) -> MoodEntry? { + let startDate = Calendar.current.startOfDay(for: date) + let endDate = Calendar.current.date(byAdding: .day, value: 1, to: startDate)! + + let predicate = NSPredicate(format: "forDate >= %@ && forDate <= %@ ", + startDate as NSDate, + endDate as NSDate) + + let fetchRequest = NSFetchRequest(entityName: "MoodEntry") + fetchRequest.predicate = predicate + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "forDate", ascending: true)] + let data = try! viewContext.fetch(fetchRequest) + return data.first + } + + public func getData(startDate: Date, endDate: Date, includedDays: [Int]) -> [MoodEntry] { + try! viewContext.setQueryGenerationFrom(.current) + viewContext.refreshAllObjects() + + var includedDays16 = [Int16]() + + if includedDays.isEmpty { + includedDays16 = [Int16(1), Int16(2), Int16(3), Int16(4), Int16(5), Int16(6), Int16(7)] + } else { + includedDays16 = includedDays.map({ + Int16($0) + }) + } + let predicate = NSPredicate(format: "forDate >= %@ && forDate <= %@ && weekDay IN %@", + startDate as NSDate, + endDate as NSDate, + includedDays16) + + let fetchRequest = NSFetchRequest(entityName: "MoodEntry") + fetchRequest.predicate = predicate + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "forDate", ascending: true)] + let data = try! viewContext.fetch(fetchRequest) + return data + } + + public func splitIntoYearMonth() -> [Int: [Int: [MoodEntry]]] { + let data = PersistenceController.shared.getData(startDate: Date(timeIntervalSince1970: 0), + endDate: Date(), + includedDays: [1,2,3,4,5,6,7]).sorted(by: { + $0.forDate! < $1.forDate! + }) + var returnData = [Int: [Int: [MoodEntry]]]() + + if let earliestEntry = data.first, + let lastEntry = data.last { + + let calendar = Calendar.current + let components = calendar.dateComponents([.year], from: earliestEntry.forDate!) + let earliestYear = components.year! + + let latestComponents = calendar.dateComponents([.year], from: lastEntry.forDate!) + let latestYear = latestComponents.year! + + for year in earliestYear...latestYear { + var allMonths = [Int: [MoodEntry]]() + + for month in (1...12) { + var components = DateComponents() + components.month = month + components.year = year + let startDateOfMonth = Calendar.current.date(from: components)! + + let items = data.filter({ entry in + let components = calendar.dateComponents([.month, .year], from: startDateOfMonth) + let entryComponents = calendar.dateComponents([.month, .year], from: entry.forDate!) + return (components.month == entryComponents.month && components.year == entryComponents.year) + }) + if !items.isEmpty { + allMonths[month] = items + } + } + returnData[year] = allMonths + } + } + return returnData + } +} diff --git a/Shared/Persisence/PersistenceHelper.swift b/Shared/Persisence/PersistenceHelper.swift new file mode 100644 index 0000000..678c383 --- /dev/null +++ b/Shared/Persisence/PersistenceHelper.swift @@ -0,0 +1,85 @@ +// +// PersistenceHelper.swift +// Feels (iOS) +// +// Created by Trey Tartt on 2/17/22. +// + +import CoreData + +extension PersistenceController { + private var childContext: NSManagedObjectContext { + return NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) + } + + public func randomEntries(count: Int) -> [MoodEntry] { + var entries = [MoodEntry]() + + for idx in 0.. [MoodEntry] { +// let predicate = NSPredicate(format: "forDate == %@", date as NSDate) + + let fetchRequest = NSFetchRequest(entityName: "MoodEntry") +// fetchRequest.predicate = predicate + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "forDate", ascending: true)] + let data = try! viewContext.fetch(fetchRequest) + return data + } +} diff --git a/Shared/Persistence.swift b/Shared/Persistence.swift deleted file mode 100644 index 3eb6f0c..0000000 --- a/Shared/Persistence.swift +++ /dev/null @@ -1,340 +0,0 @@ -// -// Persistence.swift -// Shared -// -// Created by Trey Tartt on 1/5/22. -// - -import CoreData -import SwiftUI - -class PersistenceController { - @AppStorage(UserDefaultsStore.Keys.useCloudKit.rawValue, store: GroupUserDefaults.groupDefaults) private var useCloudKit = false - - static let shared = PersistenceController.persistenceController - - private static var persistenceController: PersistenceController { - return PersistenceController(inMemory: false) - } - - public var viewContext: NSManagedObjectContext { - return PersistenceController.shared.container.viewContext - } - - private var childContext: NSManagedObjectContext { - return NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) - } - - - public var switchContainerListeners = [(() -> Void)]() - - public func getEntry(byDate date: Date) -> MoodEntry? { - let predicate = NSPredicate(format: "forDate == %@", - date as NSDate) - - let fetchRequest = NSFetchRequest(entityName: "MoodEntry") - fetchRequest.predicate = predicate - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "forDate", ascending: true)] - let data = try! viewContext.fetch(fetchRequest) - return data.first - } - - public func add(mood: Mood, forDate date: Date, entryType: EntryType) { - if let existingEntry = getEntry(byDate: date) { - viewContext.delete(existingEntry) - try? viewContext.save() - } - - let newItem = MoodEntry(context: viewContext) - newItem.timestamp = Date() - newItem.moodValue = Int16(mood.rawValue) - newItem.forDate = date - newItem.weekDay = Int16(Calendar.current.component(.weekday, from: date)) - newItem.canEdit = true - newItem.canDelete = true - newItem.entryType = Int16(entryType.rawValue) - - do { - try viewContext.save() - } catch { - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } - } - - public var earliestEntry: MoodEntry? { - let fetchRequest = NSFetchRequest(entityName: "MoodEntry") - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "forDate", ascending: true)] - let first = try! viewContext.fetch(fetchRequest).first - return first ?? nil - } - - public var latestEntry: MoodEntry? { - let fetchRequest = NSFetchRequest(entityName: "MoodEntry") - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "forDate", ascending: true)] - let last = try! viewContext.fetch(fetchRequest).last - return last ?? nil - } - - public func getData(startDate: Date, endDate: Date, includedDays: [Int]) -> [MoodEntry] { - try! viewContext.setQueryGenerationFrom(.current) - viewContext.refreshAllObjects() - - var includedDays16 = [Int16]() - - if includedDays.isEmpty { - includedDays16 = [Int16(1), Int16(2), Int16(3), Int16(4), Int16(5), Int16(6), Int16(7)] - } else { - includedDays16 = includedDays.map({ - Int16($0) - }) - } - let predicate = NSPredicate(format: "forDate >= %@ && forDate <= %@ && weekDay IN %@", - startDate as NSDate, - endDate as NSDate, - includedDays16) - - let fetchRequest = NSFetchRequest(entityName: "MoodEntry") - fetchRequest.predicate = predicate - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "forDate", ascending: true)] - let data = try! viewContext.fetch(fetchRequest) - return data - } - - func populateTestData() { - for idx in 1..<120 { - let newItem = MoodEntry(context: viewContext) - newItem.timestamp = Date() - newItem.moodValue = Int16(Mood.allValues.randomElement()!.rawValue) - newItem.canEdit = true - newItem.canDelete = true - - let date = Calendar.current.date(byAdding: .day, value: -idx, to: Date())! - newItem.forDate = date - newItem.weekDay = Int16(Calendar.current.component(.weekday, from: date)) - } - do { - try viewContext.save() - } catch { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } - } - - func populateMemory() { - for idx in 1..<25 { - let newItem = MoodEntry(context: viewContext) - newItem.timestamp = Calendar.current.date(byAdding: .day, value: -idx, to: Date()) - newItem.moodValue = Int16(Mood.allValues.randomElement()!.rawValue) - newItem.canEdit = true - newItem.canDelete = true - - let date = Calendar.current.date(byAdding: .day, value: -idx, to: Date())! - newItem.forDate = date - newItem.weekDay = Int16(Calendar.current.component(.weekday, from: date)) - } - do { - try viewContext.save() - } catch { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } - } - - func fillInMissingDates() { - let latestVoteUnLocked = UserDefaultsStore.getOnboarding().ableToVoteBasedOnCurentTime() - let inputDay = UserDefaultsStore.getOnboarding().inputDay - - var endDate: Date? - - switch (latestVoteUnLocked, inputDay) { - case (true, .Previous): - endDate = Calendar.current.date(byAdding: .day, value: -1, to: Date())! - case (true, .Today): - endDate = Date() - case (false, .Previous): - endDate = Calendar.current.date(byAdding: .day, value: -2, to: Date())! - case (false, .Today): - endDate = Calendar.current.date(byAdding: .day, value: -1, to: Date())! - } - - let fetchRequest = NSFetchRequest(entityName: "MoodEntry") - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "forDate", ascending: false)] - let entries = try! viewContext.fetch(fetchRequest) - - if let firstEntry = entries.last?.forDate { - let allDates: [Date] = Date.dates(from: firstEntry, to: endDate!).map({ - let zeroDate = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: $0)! - return zeroDate - }) - - let existingEntries: [Date] = entries.compactMap({ - if let date = $0.forDate { - let zeroDate = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: date)! - return zeroDate - } - return nil - }) - - let allDatesSet = Set(allDates) - let existingEntriesSet = Set(existingEntries) - let missing = Array(allDatesSet.subtracting(existingEntriesSet)).sorted(by: >) - for date in missing { - add(mood: .missing, forDate: date, entryType: .listView) - } - } - } - - func clearDB() { - let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "MoodEntry") - let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) - - do { - try viewContext.executeAndMergeChanges(using: deleteRequest) - try viewContext.save() - } catch let error as NSError { - fatalError("Unresolved error \(error), \(error.userInfo)") - } - } - - public func randomEntries(count: Int) -> [MoodEntry] { - var entries = [MoodEntry]() - - for idx in 0.. NSPersistentContainer { - if useCloudKit { - container = NSPersistentCloudKitContainer(name: "Feels") - } else { - container = NSCustomPersistentContainer(name: "Feels") - } - - for description in container.persistentStoreDescriptions { - description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey) - description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey) - description.setOption(true as NSNumber, forKey: NSMigratePersistentStoresAutomaticallyOption) - description.setOption(true as NSNumber, forKey: NSInferMappingModelAutomaticallyOption) - } - - container.loadPersistentStores(completionHandler: { (storeDescription, error) in - self.container.viewContext.automaticallyMergesChangesFromParent = true - - if let error = error as NSError? { - fatalError("Unresolved error \(error), \(error.userInfo)") - } - }) - - return container - } - - init(inMemory: Bool = false) { - container = setupContainer() - } - - public func splitIntoYearMonth() -> [Int: [Int: [MoodEntry]]] { - let data = PersistenceController.shared.getData(startDate: Date(timeIntervalSince1970: 0), - endDate: Date(), - includedDays: [1,2,3,4,5,6,7]).sorted(by: { - $0.forDate! < $1.forDate! - }) - var returnData = [Int: [Int: [MoodEntry]]]() - - if let earliestEntry = data.first, - let lastEntry = data.last { - - let calendar = Calendar.current - let components = calendar.dateComponents([.year], from: earliestEntry.forDate!) - let earliestYear = components.year! - - let latestComponents = calendar.dateComponents([.year], from: lastEntry.forDate!) - let latestYear = latestComponents.year! - - for year in earliestYear...latestYear { - var allMonths = [Int: [MoodEntry]]() - - for month in (1...12) { - var components = DateComponents() - components.month = month - components.year = year - let startDateOfMonth = Calendar.current.date(from: components)! - - let items = data.filter({ entry in - let components = calendar.dateComponents([.month, .year], from: startDateOfMonth) - let entryComponents = calendar.dateComponents([.month, .year], from: entry.forDate!) - return (components.month == entryComponents.month && components.year == entryComponents.year) - }) - if !items.isEmpty { - allMonths[month] = items - } - } - returnData[year] = allMonths - } - } - return returnData - } -} - -extension NSManagedObjectContext { - - /// Executes the given `NSBatchDeleteRequest` and directly merges the changes to bring the given managed object context up to date. - /// - /// - Parameter batchDeleteRequest: The `NSBatchDeleteRequest` to execute. - /// - Throws: An error if anything went wrong executing the batch deletion. - public func executeAndMergeChanges(using batchDeleteRequest: NSBatchDeleteRequest) throws { - batchDeleteRequest.resultType = .resultTypeObjectIDs - let result = try execute(batchDeleteRequest) as? NSBatchDeleteResult - let changes: [AnyHashable: Any] = [NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []] - NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [self]) - } -} - -extension PersistenceController { - func longestStreak() -> [MoodEntry] { -// let predicate = NSPredicate(format: "forDate == %@", date as NSDate) - - let fetchRequest = NSFetchRequest(entityName: "MoodEntry") -// fetchRequest.predicate = predicate - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "forDate", ascending: true)] - let data = try! viewContext.fetch(fetchRequest) - return data - } -} - -class NSCustomPersistentContainer: NSPersistentContainer { - override open class func defaultDirectoryURL() -> URL { - var storeURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Constants.groupShareId) - storeURL = storeURL?.appendingPathComponent("Feels.sqlite") -#if DEBUG - storeURL = storeURL?.appendingPathComponent("Feels-Debug.sqlite") -#endif - return storeURL! - } -} diff --git a/Shared/ShowBasedOnVoteLogics.swift b/Shared/ShowBasedOnVoteLogics.swift new file mode 100644 index 0000000..9beb00c --- /dev/null +++ b/Shared/ShowBasedOnVoteLogics.swift @@ -0,0 +1,123 @@ +// +// ShowBasedOnVoteLogics.swift +// Feels (iOS) +// +// Created by Trey Tartt on 2/17/22. +// + +import CoreData +import SwiftUI + + +//if this is being shown we're missing an entry +// voting time is noon + // vote for current day + // today at 11 am -> How as yesterday + // today at 1 pm -> How is today + // vote for previous day + // today at 11 am -> How as 2 days ago + // today at 1 pm -> How was yesterday +class ShowBasedOnVoteLogics { + private static var currentVoting: (passTimeToVote: Bool, dayOptions: DayOptions) { + let passedTimeToVote = UserDefaultsStore.getOnboarding().ableToVoteBasedOnCurentTime() + let inputDay = UserDefaultsStore.getOnboarding().inputDay + + return (passedTimeToVote, inputDay) + } + + static func isMissingCurrentVote() -> Bool { + var startDate: Date? + + switch (currentVoting.passTimeToVote, currentVoting.dayOptions) { + case (true, .Previous): + // if we're passed time to vote and the voting type is previous - last vote should be -1 + startDate = Calendar.current.date(byAdding: .day, value: -1, to: Date())! + case (true, .Today): + // if we're passed time to vote and the voting type is today - last vote should be current date + startDate = Date() + case (false, .Previous): + // if we're NOT passed time to vote and the voting type is previous - last vote should be 2 days ago + startDate = Calendar.current.date(byAdding: .day, value: -2, to: Date())! + case (false, .Today): + // if we're NOT passed time to vote and the voting type is previous - last vote should be -1 + startDate = Calendar.current.date(byAdding: .day, value: -1, to: Date())! + } + + startDate = Calendar.current.startOfDay(for: startDate!) + let endDate = Calendar.current.date(byAdding: .day, value: 1, to: startDate!)! + + let fetchRequest = NSFetchRequest(entityName: "MoodEntry") + let fromPredicate = NSPredicate(format: "%@ <= %K", startDate! + as NSDate, #keyPath(MoodEntry.forDate)) + let toPredicate = NSPredicate(format: "%K < %@", #keyPath(MoodEntry.forDate), endDate as NSDate) + let datePredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [fromPredicate, toPredicate]) + fetchRequest.predicate = datePredicate + let entries = try! PersistenceController.shared.viewContext.count(for: fetchRequest) + + return entries < 1 + } + + static func getVotingTitle() -> String { + switch (currentVoting.passTimeToVote, currentVoting.dayOptions) { + case (true, .Previous): + // if we're passed time to vote and the voting type is previous - last vote should be -1 + return "how was yesterday" + case (true, .Today): + // if we're passed time to vote and the voting type is previous - last vote should be today + return "how is today" + case (false, .Previous): + // if we're passed time to vote and the voting type is previous - last vote should be -2 + let lastDayVoteShouldExist = ShowBasedOnVoteLogics.getLastDateVoteShouldExist() + return "how was \(Random.weekdayName(fromDate: lastDayVoteShouldExist))" + case (false, .Today): + // if we're passed time to vote and the voting type is previous - last vote should be -1 + return "how as yesterday" + } + } + + static func dateForHeaderVote() -> Date? { + var date: Date? + + switch (currentVoting.passTimeToVote, currentVoting.dayOptions) { + case (true, .Previous): + // if we're passed time to vote and the voting type is previous - last vote should be -1 + date = Calendar.current.date(byAdding: .day, value: -1, to: Date()) + case (true, .Today): + // if we're passed time to vote and the voting type is previous - last vote should be today + date = Date() + case (false, .Previous): + // if we're passed time to vote and the voting type is previous - last vote should be -2 + date = Calendar.current.date(byAdding: .day, value: -2, to: Date()) + case (false, .Today): + // if we're passed time to vote and the voting type is previous - last vote should be -1 + date = Calendar.current.date(byAdding: .day, value: -1, to: Date()) + } + date = Calendar.current.date(byAdding: .day, value: -2, to: Date()) + if let date = date { + return date + } + + return nil + } + + static func getLastDateVoteShouldExist() -> Date { + var endDate: Date? + + switch (currentVoting.passTimeToVote, currentVoting.dayOptions) { + case (true, .Previous): + // if we're passed time to vote and the voting type is previous - last vote should -1 + endDate = Calendar.current.date(byAdding: .day, value: -1, to: Date())! + case (true, .Today): + // if we're passed time to vote and the voting type is previous - last vote should be today + endDate = Date() + case (false, .Previous): + // if we're passed time to vote and the voting type is previous - last vote should be -2 + endDate = Calendar.current.date(byAdding: .day, value: -2, to: Date())! + case (false, .Today): + // if we're passed time to vote and the voting type is previous - last vote should be -1 + endDate = Calendar.current.date(byAdding: .day, value: -1, to: Date())! + } + + return endDate! + } +} diff --git a/Shared/views/AddMoodHeaderView.swift b/Shared/views/AddMoodHeaderView.swift index e83fe9f..fd421aa 100644 --- a/Shared/views/AddMoodHeaderView.swift +++ b/Shared/views/AddMoodHeaderView.swift @@ -25,7 +25,7 @@ struct AddMoodHeaderView: View { Color(theme.currentTheme.secondaryBGColor) VStack { - Text(self.getTitle()) + Text(ShowBasedOnVoteLogics.getVotingTitle()) .font(.title) .foregroundColor(Color(UIColor.label)) .padding() @@ -56,48 +56,8 @@ struct AddMoodHeaderView: View { .frame(minWidth: 0, maxWidth: .infinity) } - private func getTitle() -> String { - //if this is being shown we're missing an entry - // voting time is noon - // vote for current day - // today at 11 am -> How as yesterday - // today at 1 pm -> How is today - // vote for previous day - // today at 11 am -> How as 2 days ago - // today at 1 pm -> How was yesterday - - let latestVoteUnLocked = UserDefaultsStore.getOnboarding().ableToVoteBasedOnCurentTime() - let inputDay = UserDefaultsStore.getOnboarding().inputDay - - switch (latestVoteUnLocked, inputDay) { - case (true, .Previous): - return "how was yesterday" - case (true, .Today): - return "how is today" - case (false, .Previous): - return "how was two days ago" - case (false, .Today): - return "how as yesterday" - } - } - private func addItem(withMood mood: Mood) { - let latestVoteUnLocked = UserDefaultsStore.getOnboarding().ableToVoteBasedOnCurentTime() - let inputDay = UserDefaultsStore.getOnboarding().inputDay - - var date: Date? - - switch (latestVoteUnLocked, inputDay) { - case (true, .Previous): - date = Calendar.current.date(byAdding: .day, value: -1, to: Date())! - case (true, .Today): - date = Date() - case (false, .Previous): - date = Calendar.current.date(byAdding: .day, value: -2, to: Date())! - case (false, .Today): - date = Calendar.current.date(byAdding: .day, value: -1, to: Date())! - } - if let date = date { + if let date = ShowBasedOnVoteLogics.dateForHeaderVote() { addItemHeaderClosure(mood, date) } } diff --git a/Shared/views/ContentView.swift b/Shared/views/ContentView.swift index 87e65e2..e708277 100644 --- a/Shared/views/ContentView.swift +++ b/Shared/views/ContentView.swift @@ -91,8 +91,7 @@ struct ContentView: View { } if let selectedEntry = selectedEntry, - deleteEnabled, - selectedEntry.mood != .missing { + deleteEnabled{ Button(String(localized: "content_view_delete_entry"), action: { viewModel.update(entry: selectedEntry, toMood: Mood.missing) showUpdateEntryAlert = false @@ -140,7 +139,11 @@ struct ContentView: View { return "" } - let components = Calendar.current.dateComponents([.day, .month, .year], from: entry.forDate!) + 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! @@ -178,7 +181,7 @@ struct ContentView: View { private var headerView: some View { VStack { - if viewModel.shouldShowVotingHeader() { + if ShowBasedOnVoteLogics.isMissingCurrentVote() { AddMoodHeaderView(addItemHeaderClosure: { (mood, date) in withAnimation { viewModel.add(mood: mood, forDate: date, entryType: .header)