Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NSPersistentStoreRemoteChangeNotification not getting fired

I am trying to perform history tracking in my CoreData+CloudKit project which uses NSPersistentCloudKitContainer. I have been following along with Apple's sample project

I want to perform certain task when the remote store has been updated. For this apple recommends enabling remote notification in the Signing & capabilities's Background Mode section of the app.

I have enabled History Tracking for my project as shown in Apple's sample project.

    // turn on persistent history tracking
    let description = container.persistentStoreDescriptions.first
    description?.setOption(true as NSNumber,
                           forKey: NSPersistentHistoryTrackingKey)

    // ...

Also I have registered my store to listen for store changes.

    // turn on remote change notifications
    let remoteChangeKey = "NSPersistentStoreRemoteChangeNotificationOptionKey"
    description?.setOption(true as NSNumber,
                               forKey: remoteChangeKey)

    // ...

Observer is also added to listen for NSPersistentStoreRemoteChangeNotification.

However there is no NSPersistentStoreRemoteChangeNotification being fired. To make sure there is no mistake in my implementation, I am have simply put breakpoints in @objc func storeRemoteChange(_ notification: Notification) the Apple's provided sample code but still I can not see any notification being fired and no breakpoints are activated.

I have understood the deduplication of the Tags done in the sample project and also tried testing it but without any success. Is it a bug in the Apple's implementation or am I missing any setup which is required?

like image 224
FE_Tech Avatar asked Dec 03 '19 13:12

FE_Tech


3 Answers

SwiftUI

Here's a way to be notified of CloudKit remote changes in a SwiftUI view, and, say, update the contents of a List that would depend on a @FetchRequest--not shown in the code for simplicity:

struct MyView: View {
    @State var refresh = UUID()
    var didRemoteChange = NotificationCenter.default.publisher(for: .NSPersistentStoreRemoteChange).receive(on: RunLoop.main)
    var body: some View {
        List {
            // ...
        }
        .id(refresh)
        .onReceive(self.didRemoteChange) { _ in
            self.refresh = UUID()
        }
    }
}

Note: .receive(on: RunLoop.main) is necessary in order to avoid modifying the UI from a background thread, as the remote event could (and will) otherwise fire from a background thread. Alternatively, .receive(on: DispatchQueue.main) can also be used.

For that to work, the NSPersistentCloudKitContainer needs to be set up to fire events when remote changes occur:

struct PersistenceController {
    static let shared = PersistenceController()
    let container: NSPersistentCloudKitContainer
    init(inMemory: Bool = false) {
        container = NSPersistentCloudKitContainer(name: "YourApp")
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        //
        // Generate notifications upon remote changes
        //
        container.persistentStoreDescriptions.forEach {
            $0.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
        }
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        container.viewContext.automaticallyMergesChangesFromParent = true
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
    }
}
like image 58
Didier B. Avatar answered Oct 22 '22 05:10

Didier B.


My guess is you are observing the container instead of the store coordinator, add your observer like this:

    NotificationCenter.default.addObserver(
        self, selector: #selector(type(of: self).storeRemoteChange(_:)),
        name: .NSPersistentStoreRemoteChange, object: container.persistentStoreCoordinator)

Note the last param container.persistentStoreCoordinator

And a warning, this notification comes in on all different threads so you be careful with concurrency. Just put a 5 second sleep in the method and you'll see on app launch 3 different threads call it. This is likely why in the example there is a historyQueue with maxOperationCount 1 to handle it.

Some notifications have NSPersistentHistoryTokenKey in the userInfo not sure why.

like image 17
malhal Avatar answered Nov 07 '22 09:11

malhal


Debugging the sample app mentioned by the OP, I observed the following:

  • As of XCode Version 11.3 (11C29), there are SDK constants both for the option key (NSPersistentStoreRemoteChangeNotificationPostOptionKey) and for the notification name (.NSPersistentStoreRemoteChange), and these are reflected in the latest download of the sample code.
  • The sample app registers for the remote change notifications on the wrong object, so it never receives any. Changing the sender as per the accepted answer fixes this.
  • The app UI always updates to reflect changes received from the cloud, but those updates are prompted not by remote change notifications but by the app's NSFetchedResultsController delegate using the controllerDidChangeContent callback to refresh the UI.
  • The standard NSPersistentCloudKitContainer used by the sample app is doing automatic imports into the local persistent store of all the cloud-sent updates and, because the persistentStore is set up for history tracking and the viewContext is set up to auto-update to the latest generation of data, each import triggers a UI update.

Based on these observations, I wrote a small app from scratch based on the XCode template you get by specifying use of CoreData, CloudKit, and SwiftUI. I set up its persistent container and view context the same way they are set up in the sample app, and used SwiftUI's @FetchRequest wrapper to obtain the data in the master view display. Sure enough, I saw the exact same remote import behavior without using any remote change notifications, and the UI updated after each import.

I then confirmed that, as per the accepted answer, if I registered for remote change notifications correctly, they would be received. They seem to be sent after each receive and import operation in the NSPersistentCloudKit completes. Observing them is not needed to get notifications of the local data changes initiated by those imports.

like image 4
brotskydotcom Avatar answered Nov 07 '22 08:11

brotskydotcom