iosswiftcore-datacloudkitnspersistentcloudkitcontainer

Core Data+CloudKit not connecting to CloudKit


I am updating a pre-iOS13 Core Data app to use Core Data+CloudKit syncing to support single users on multiple devices. The syncing is supposed to occur automagically, and in an interim step in my development it did work. Now it's not working, with CloudKit telemetry not registering any activity, and I can't figure out why it's not working.

In my previous app versions I provided a small number of label strings in UserDefaults and allowed users to modify them, with the updated versions put back into UserDefaults. It was a shortcut to avoid having to manage a second Core Data entity for what would only ever be a small number of objects.

I have since realized this won't work in a multi-device implementation because the fact that the local Core Data database is empty no longer means it's the first use for that user. Instead, each new device needs to look to the cloud-based data source to find the user's in-use label strings, and only use app-provided default values if the user doesn't already have something else.

I followed Apple's instructions for Setting Up Core Data with CloudKit, and at first syncing worked fine. But then I realized the syncing behavior wasn't correct, and that instead of pre-populating from strings stored in UserDefaults I really needed to provide a pre-populated Core Data database (.sqlite files). I implemented that and the app now works fine locally after copying the bundled .sqlite files at first local launch.

But for some reason this change caused the CloudKit syncing to stop working. Now, not only do I not get any automagical updates on other devices, I get no results in the CloudKit dashboard telemetry so it appears that CloudKit synching never gets started. This is odd because locally I am getting notifications of 'remote' changes that just occurred locally (the function I list as a #selector for the notification is being called locally when I save new data locally).

I'm stumped as to what the problem/solution is. Here is my relevant code.

//In my CoreDataHelper class
lazy var context = persistentContainer.viewContext

lazy var persistentContainer: NSPersistentCloudKitContainer = {

    let appName = Bundle.main.infoDictionary!["CFBundleName"] as! String
    let container = NSPersistentCloudKitContainer(name: appName)
    
    //Pre-load my default Core Data data (Category names) on first launch
    let storeUrl = FileManager.default.urls(for: .applicationSupportDirectory, in:.userDomainMask).first!.appendingPathComponent(appName + ".sqlite")
    let storeUrlFolder = FileManager.default.urls(for: .applicationSupportDirectory, in:.userDomainMask).first!

    if !FileManager.default.fileExists(atPath: (storeUrl.path)) {
        let seededDataUrl = Bundle.main.url(forResource: appName, withExtension: "sqlite")
        let seededDataUrl2 = Bundle.main.url(forResource: appName, withExtension: "sqlite-shm")
        let seededDataUrl3 = Bundle.main.url(forResource: appName, withExtension: "sqlite-wal")

        try! FileManager.default.copyItem(at: seededDataUrl!, to: storeUrl)
        try! FileManager.default.copyItem(at: seededDataUrl2!, to: storeUrlFolder.appendingPathComponent(appName + ".sqlite-shm"))
        try! FileManager.default.copyItem(at: seededDataUrl3!, to: storeUrlFolder.appendingPathComponent(appName + ".sqlite-wal"))
    }

    let storeDescription = NSPersistentStoreDescription(url: storeUrl)
    storeDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)

    //In the view controllers, we'll listen for relevant remote changes
    let remoteChangeKey = "NSPersistentStoreRemoteChangeNotificationOptionKey"
    storeDescription.setOption(true as NSNumber, forKey: remoteChangeKey)

    container.persistentStoreDescriptions = [storeDescription]
    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
        if let error = error as NSError? {
            fatalError("Unresolved error \(error), \(error.userInfo)")
        }
    })
    
    //This is returning nil but I don't think it should
    print(storeDescription.cloudKitContainerOptions?.containerIdentifier)
    
    return container
}()

//In my view controller

let context = CoreDataHelper.shared.context

override func viewDidLoad() {
    super.viewDidLoad()
    
    //Do other setup stuff, removed for clarity
    
    //enable CloudKit syncing
    context.automaticallyMergesChangesFromParent = true
}

override func viewWillAppear(_ animated: Bool) {
    clearsSelectionOnViewWillAppear = splitViewController!.isCollapsed
    super.viewWillAppear(animated)
    
    NotificationCenter.default.addObserver(
        self,
        selector: #selector(reportCKchange),
        name: NSNotification.Name(
            rawValue: "NSPersistentStoreRemoteChangeNotification"),
        object: CoreDataHelper.shared.persistentContainer.persistentStoreCoordinator
    )
    updateUI()
}

@objc
fileprivate func reportCKchange() {
    print("Change reported from CK")
    tableView.reloadData()
}

Note: I have updated the target to be iOS13+.


Solution

  • I think a newly created NSPersistentStoreDescription has no cloudKitContainerOptions by default.

    To set them, try:

    storeDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: <<Your CloudKit ID>>)