10

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?

ianq
  • 117
  • 2
  • 6

1 Answers1

13

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

Paul Martin
  • 699
  • 4
  • 13
  • As you said It working fine but It will started working once restart app. – Manikandan D Nov 19 '19 at 13:20
  • Restart app is not necessary, I've tested NSPersistentContainer objects with same model object can be swapped without restarting. – Jakehao Dec 13 '19 at 13:23
  • Very helpful, please can you explain how to switch between NSPersistentContainer to NSPersistentCloudKitContainer without restart app. I’m trying a few things but with no success. Thanks in advance. – garanda Apr 09 '20 at 21:14
  • Also note that if you are setting the `NSPersistentStoreDescription` to interact with the public database rather than the private database (previous default before iOS 14 I believe) via `description.cloudKitContainerOptions?.databaseScope = .public`, it will only poll for changes every 30 minutes by design (see 10:30 into the WWDC 2020 session video on this topic: https://developer.apple.com/videos/play/wwdc2020/10650/) – professormeowingtons Nov 23 '20 at 07:18
  • @garanda were you able to figure out how to add `NSPersistentHistoryTrackingKey` to your cloudkit without an app restart? I'm having similar issues right now – fphelp Mar 10 '21 at 02:33
  • @Paul Martin, are you saying to add NSPersistentHistoryTrackingKey even when "useCloudSync" is enabled? – fphelp Mar 10 '21 at 03:13
  • @fphelp Yes, NSPersistentHistoryTrackingKey must be enabled at all times so that changes can be tracked, even if the users isn't using iCloud on that device - because they *might* turn it on. – Paul Martin Mar 27 '21 at 01:28
  • @PaulMartin - Can you please confirm that your latest edited code is working fine on your end? - I'm trying to use your latest method but for some reason, I have noticed that it's inconsistent. For instance, this is what I see when using two devices. 1- At launch Device1 and Device2 are OFF/Sync by default: No data is synced in any device (OK). 2- Device1 OFF and Device2 ON: Entering data in either device, reflects the data in the other device (ISSUE). 3- Turning both devices OFF: Syncing stops, data entered in either device is Not reflected in the other device (OK). – fs_tigre Jan 22 '22 at 19:57
  • @fs_tigre I haven't had any issues with this code lately (other than all the iCloud syncing issues with iOS15 - now resolved with 15.2 an up. I also haven't really modified this code in my App for a long time. It is embedded in a 'PersistenceService' class object if that makes any difference? I'm not sure? – Paul Martin Jan 25 '22 at 05:29
  • @PaulMartin - Thank you for the confirmation. Maybe my problem is that I'm probably not reloading the container correctly when turning it On/Off, I would love to see the code for the `PersistenceService` class and how you are using it to turn sync On/Off. Thanks – fs_tigre Jan 25 '22 at 13:38
  • It's probably not very sophisticated... and possibly poorly coded, as I'm no expert. The toggle for sync really is just the UserDefaults bool as outlined above. It sometimes requires the user to restart the app to force it to start/stop sycing for the first time – Paul Martin Feb 03 '22 at 09:23