10
votes

I am using NSPersistentCloudKitContainer for my Core Data application. During testing, I checked that changes made on the device are sync to CloudKit within second.

However, when I disabled iCloud on my device then re-enable immediately, all my data on the device disappeared. I check that the data in my private database still exist on CloudKit. It took more than 1 day before data on CloudKit are sync back to my device.

This will cause confusion to users when they change device and see that their data have disappeared at first. Question: How can I control how fast data on CloudKit is sync back to my device?

1

1 Answers

11
votes

Frustratingly, I think this is 'normal' behaviour, particularly with a large database with a large number of relationships to sync - there is no way to see the progress (to show the user) nor can you speed it up.

NSPersistentCloudKitContainer seems to treat each relationship as an individual CKRecord, with syncing still bound by the same limitations (ie. no more than 400 'requests' at a time), you'll often see these .limitExceeded errors in the Console, but with little other information (ie. if/when will it retry??).

I'm finding this results in the database taking days to sync fully, with the data looking messed up and incomplete in the meantime. I have thousands of many-to-many relationships and when a user restores their database from an external file, all those CKRecords need to be recreated.

The main concern I have here is that there is no way to query NSPersistentCloudKitContainer whether there are pending requests, how much has yet to sync, etc. so you can relay this to the users in the UI so they don't keep deleting and restoring thinking it's 'failed'.

One way around the local data being deleted when you turn off syncing - and potentially saving having to have it all 're-sync' when you turn it back on - is to use a NSPersistentContainer when it's off, and an NSPersistentCloudKitContainer when it's on.

NSPersistentCloudKitContainer is a subclass of NSPersistentContainer.

I am currently doing this in my App in my custom PersistenceService Class:

static var useCloudSync = UserDefaults.standard.bool(forKey: "useCloudSync")
static var persistentContainer:  NSPersistentContainer  = {
    let container: NSPersistentContainer?
    if useCloudSync {
        container = NSPersistentCloudKitContainer(name: "MyApp")
    } else {
        container = NSPersistentContainer(name: "MyApp")
        let description = container!.persistentStoreDescriptions.first
        description?.setOption(true as NSNumber,
                               forKey: NSPersistentHistoryTrackingKey)

    }
    container!.loadPersistentStores(completionHandler: { (storeDescription, error) in
    if let error = error as NSError? {
        fatalError("Unresolved error \(error), \(error.userInfo)")
    }
    })
    return container!
}()

This at least results in the local data being untouched when iCloud is turned off within your App and doesn't require everything being re-synced when turned back on.

I think you can also query iOS to see if the user has turned off iCloud in System Settings and switch between the two before NSPersistentCloudKitContainer deletes all the local data.

EDIT: Added the NSPersistentHistoryTrackingKey as without it, switching back to NSPersistentContainer from NSPersistentCloudKitContainer fails.

It is working properly in my app. When the user re-enables iCloud Syncing within my app (and switches from NSPersistentContainer to NSPersistentCloudKitContainer ) it syncs anything that was added/changed since the last sync which is exactly what I want!

EDIT 2: Here is a better implementation of the above

Essentially, whether the user is syncing to iCloud or not simply requires changing the .options on the container to use an NSPersistentCloudKitContainerOptions(containerIdentifier:) or nil. There appears no need to toggle between an NSPersistentCloudKitContainer and an NSPersistentContainer at all.

static var synciCloudData = {
    return defaults.bool(forKey: Settings.synciCloudData.key)
}()

static var persistentContainer:  NSPersistentContainer  = {
    let container = NSPersistentCloudKitContainer(name: "AppName")
    
    guard let description = container.persistentStoreDescriptions.first else {
        fatalError("Could not retrieve a persistent store description.")
    }
    
    description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
    
    if synciCloudData {
        let cloudKitContainerIdentifier = "iCloud.com.yourID.AppName"
        let options = NSPersistentCloudKitContainerOptions(containerIdentifier: cloudKitContainerIdentifier)
        
        description.cloudKitContainerOptions = options
    } else {
        description.cloudKitContainerOptions = nil
    }
            
    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
        if let error = error as NSError? {
            fatalError("Unresolved error \(error), \(error.userInfo)")
        }
    })
    
    return container
}()

Finally, you can see the state of iCloud syncing, albeit crudely (ie. you can't see if anything is 'in sync' only that a sync is either pending/in progress and whether it succeeded or failed. Having said that, this is enough for my use case in my App.

See the reply by user ggruen towards the bottom of this post: NSPersistentCloudKitContainer: How to check if data is synced to CloudKit