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)