// // 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 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) { 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 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 fetchRequest = NSFetchRequest(entityName: "MoodEntry") let entries = try! viewContext.fetch(fetchRequest) if let earliestDate = entries.last?.forDate, let diffInDays = Calendar.current.dateComponents([.day], from: earliestDate, to: Date()).day, diffInDays > 1 { for idx in 1.. = 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) } 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]) } } 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! } }