Search code examples
swiftcore-datapush-notificationnotificationscloudkit

CoreData and CloudKit Sync works, but NSPersistentStoreRemoteChange notification never fires


I've updated an existing CoreData app to use CloudKit so that it can keep data synchronised across multiple devices (for the same iCloud account).

It works! The data sync's just fine.

But the primary UIViewController's view does not reflect the new data. To see the changes, I have to switch to a different view, then switch back to the primary view which has a refreshData() call in its viewWillAppear() function. It then re-reads from the data store and displays all the data (including whatever was updated on some other device.

The UIViewController should be observing the NSPersistentStoreRemoteChange notification as per the viewDidLoad() below:

    override func viewDidLoad() {
        super.viewDidLoad()
    //  NotificationCenter.default.addObserver(self, selector: #selector(handleRemoteChange), name: .NSPersistentStoreRemoteChange, object: AppDelegate.appDelegate().persistentContainer.persistentStoreCoordinator)
        NotificationCenter.default.addObserver(self, selector: #selector(handleRemoteChange), name: .NSPersistentStoreRemoteChange, object: nil)
    }

But the handler function never gets called:

    @objc func handleRemoteChange(notification: Notification) {
        print("Handling remote change to data in Collection Controller")
        refreshData()
    }

That print statement never prints anything, and a breakpoint set there never fires.

I have configured the managed object context to merge changes from parent as required immediately after the NSPersistentCloutKitContainer is created:

    container.viewContext.automaticallyMergesChangesFromParent = true

The application has all the relevant iCloud capabilities and entitlements that I'm aware of after scouring the web (and Stack Overflow) for clues, including:

  • "Remote notifications" (Background Modes)
  • "CloudKit" (iCloud)
  • A container has been created and selected with an identifier of the format: iCloud.reverse.domain.app

The Container's persistentStoreDescription has the NSPersistentStoreRemoteChangeNotificationPostOptionKey option set to true:

        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                print("FAILED to load persistent store:\(storeDescription)\nERROR:\n\(error)\n\(error.userInfo)")
            }
        })

        container.viewContext.automaticallyMergesChangesFromParent = true
        for description in container.persistentStoreDescriptions {
            description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
        }

Clearly the data is being sync'd OK. It's just the notification that is not working as expected. So the end user doesn't see the new/sync'd data automatically.

How can I get this to work?


Solution

  • I have the same problem, and solved by configuring custom persistent store descriptions before calling loadPersistentStores(completionHandler:)

    container = NSPersistentCloudKitContainer(name: "RubikClub")
    // turn on persistent history tracking
    let description = container.persistentStoreDescriptions.first
    description?.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
    description?.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
    NotificationCenter.default.addObserver(self, selector: #selector(type(of: self).storeRemoteChange(_:)), name: .NSPersistentStoreRemoteChange, object: container.persistentStoreCoordinator)
    
    container.loadPersistentStores { (_, error) in
      if let error = error {
        fatalError("init core data error: \(error.localizedDescription)")
      }
    }
    container.viewContext.automaticallyMergesChangesFromParent = true