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