Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CloudKit Sync using NSPersistentCloudKitContainer in iOS13

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?

like image 707
ianq Avatar asked Oct 01 '19 07:10

ianq


People also ask

Do CloudKit apps sync with iCloud users?

Sync user data between multiple apps from the same developer If you have a suite of apps, they can all share the same CloudKit container so users can have access to their data for all of your apps on every device they have associated with their iCloud account.

What is CloudKit framework in iOS?

The CloudKit framework provides interfaces for moving data between your app and your iCloud containers. You use CloudKit to store your app's existing data in the cloud so that the user can access it on multiple devices. You can also store data in a public area where all users can access it.

What is CloudKit mirroring?

Configuring CloudKit MirroringSynchronize objects between devices, and handle store changes in the user interface. Reading CloudKit Records for Core Data. Access CloudKit records created from Core Data managed objects.


1 Answers

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

like image 99
Paul Martin Avatar answered Oct 11 '22 14:10

Paul Martin