diff --git a/Tusker/CoreData/MastodonCachePersistentStore.swift b/Tusker/CoreData/MastodonCachePersistentStore.swift index 1b8c030a..63598f05 100644 --- a/Tusker/CoreData/MastodonCachePersistentStore.swift +++ b/Tusker/CoreData/MastodonCachePersistentStore.swift @@ -39,6 +39,9 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer { return context }() + private var remoteChangeHandlerQueue = DispatchQueue(label: "PersistentStore remote changes") + private var lastRemoteChangeToken: NSPersistentHistoryToken? + // TODO: consider sending managed objects through this to avoid re-fetching things unnecessarily // would need to audit existing uses to make sure everything happens on the main thread // and when updating things on the background context would need to switch to main, refetch, and then publish @@ -63,6 +66,8 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer { localStoreLocation.appendPathComponent("\(accountInfo!.persistenceKey)_cache.sqlite", isDirectory: false) let localStoreDescription = NSPersistentStoreDescription(url: localStoreLocation) localStoreDescription.configuration = "Local" + localStoreDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey) + localStoreDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey) var cloudStoreLocation = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! cloudStoreLocation.appendPathComponent("cloud.sqlite", isDirectory: false) @@ -71,6 +76,8 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer { let options = NSPersistentCloudKitContainerOptions(containerIdentifier: "iCloud.space.vaccor.Tusker") options.databaseScope = .private cloudStoreDescription.cloudKitContainerOptions = options + cloudStoreDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey) + cloudStoreDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey) persistentStoreDescriptions = [ cloudStoreDescription, @@ -167,6 +174,7 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer { viewContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump NotificationCenter.default.addObserver(self, selector: #selector(managedObjectsDidChange), name: .NSManagedObjectContextObjectsDidChange, object: viewContext) + NotificationCenter.default.addObserver(self, selector: #selector(remoteChanges), name: .NSPersistentStoreRemoteChange, object: persistentStoreCoordinator) } func save(context: NSManagedObjectContext) { @@ -475,4 +483,45 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer { return changes } + // the remote change notifications only handle deletes, inserts get handled by the regular managed object did change notifications + @objc private func remoteChanges(_ notification: Foundation.Notification) { + guard let token = notification.userInfo?[NSPersistentHistoryTokenKey] as? NSPersistentHistoryToken else { + return + } + remoteChangeHandlerQueue.async { + defer { + self.lastRemoteChangeToken = token + } + let req = NSPersistentHistoryChangeRequest.fetchHistory(after: self.lastRemoteChangeToken) + self.backgroundContext.performAndWait { + if let result = try? self.backgroundContext.execute(req) as? NSPersistentHistoryResult, + let transactions = result.result as? [NSPersistentHistoryTransaction] { + var changes: (hashtags: Bool, instances: Bool) = (false, false) + outer: for transaction in transactions { + for change in transaction.changes ?? [] { + if change.changedObjectID.entity.name == "SavedHashtag" { + changes.hashtags = true + } else if change.changedObjectID.entity.name == "SavedInstance" { + changes.instances = true + } + if changes.hashtags && changes.instances { + break outer + } + } + } + if changes.hashtags { + DispatchQueue.main.async { + NotificationCenter.default.post(name: .savedHashtagsChanged, object: nil) + } + } + if changes.instances { + DispatchQueue.main.async { + NotificationCenter.default.post(name: .savedInstancesChanged, object: nil) + } + } + } + } + } + } + }