Toggle sync with NSPersistentCloudKitContainer

Is there a way to pause or disable syncing via NSPersistentCloudKitContainer at runtime? I want to give my users the option to enable or disable syncing, but there doesn't seem to be a good way to do it. I tried reloading the persistent stores and setting the store description's cloudKitContainerOptions to nil, but if I do that after first initialization, I get an error:


error: Store opened without NSPersistentHistoryTrackingKey but previously had been opened with NSPersistentHistoryTrackingKey - Forcing into Read Only mode


Anyone know of another way to do this?


I submitted a request via Feedback Assistant for a flag to enable or disable syncing (FB6156182). I know the user could theoretically go into the Settings app and enable or disable iCloud access for the app as a whole, but that's not very discoverable, and disables all iCloud access. I'm looking for something a little more fine-grained (limited to the NSPersistentCloudKitContainer) and that I could control in my app's UI.

Accepted Reply

That error is because you also removed the history tracking option. Which you shouldn't do after you've enabled it.


You can disable CloudKit sync simply by setting the cloudKitContainer options property on your store description to nil.


However, you should leave history tracking on so that NSPersitentCloudKitContainer can catch up if you turn it on again.

  • Thank you. One of the advice is, we should purge history tracking transactions after certain time frame. However, if user has disabled cloudkit sync, does this mean we shouldn't perform such an operation? As, if we do so, and later user turn on cloudkit sync again, he might not able to catch up due to lack of enough history tracking transactions info.

    But, this is causing a dilemma. If we never clean the history tracking transactions, will it cause disk full issue?

    Thanks,

Add a Comment

Replies

That error is because you also removed the history tracking option. Which you shouldn't do after you've enabled it.


You can disable CloudKit sync simply by setting the cloudKitContainer options property on your store description to nil.


However, you should leave history tracking on so that NSPersitentCloudKitContainer can catch up if you turn it on again.

  • Thank you. One of the advice is, we should purge history tracking transactions after certain time frame. However, if user has disabled cloudkit sync, does this mean we shouldn't perform such an operation? As, if we do so, and later user turn on cloudkit sync again, he might not able to catch up due to lack of enough history tracking transactions info.

    But, this is causing a dilemma. If we never clean the history tracking transactions, will it cause disk full issue?

    Thanks,

Add a Comment

How should we go about leaving history tracking enabled once we set the cloudKitContainer options to nil?

How do you set the store description to nil.....I see its key/value but I tried to set it on the description after loading the store and it says its read only?


// MARK: - Core Data stack

lazy var persistentContainer: NSPersistentCloudKitContainer = {

/*

The persistent container for the application. This implementation

creates and returns a container, having loaded the store for the

application to it. This property is optional since there are legitimate

error conditions that could cause the creation of the store to fail.

*/

let container = NSPersistentCloudKitContainer(name: "name")

container.loadPersistentStores(completionHandler: { (storeDescription, error) in

{tried to set it here storeDescription.options = nil}

Did you get this to work? I have a similar need, that is, to offer iCloud storage and sync as a premium feature. I have CloudKit sync working but how do I switch it off? I want to disable CloudKit sync and only enable it when a customer pays for the premium version. I've tried setting the container's cloudKitContainerOptions property to nil but CloudKit sync still happens.


Here's my Core Data stack:


private lazy var persistentContainer: NSPersistentContainer = {

let container = NSPersistentCloudKitContainer(name: modelName)

let localDescription = NSPersistentStoreDescription(url: configurationsURL.appendingPathComponent("local.sqlite", isDirectory: false))

localDescription.configuration = "Local"

let cloudDescription = NSPersistentStoreDescription(url: configurationsURL.appendingPathComponent("cloud.sqlite", isDirectory: false))

cloudDescription.configuration = "Cloud"

cloudDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: cloudKitContainerIdentifier)

cloudDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)

cloudDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)

container.persistentStoreDescriptions = [localDescription, cloudDescription]


container.loadPersistentStores { (storeDescription, error) in

if let error = error as NSError? {

fatalError("###\(#function): Failed to load persistent stores:\(error)")

}

}

container.viewContext.name = "main"

container.viewContext.transactionAuthor = appTransactionAuthorName

container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy

container.viewContext.automaticallyMergesChangesFromParent = true

do {

try container.viewContext.setQueryGenerationFrom(.current)

} catch {

fatalError("###\(#function): Failed to pin viewContext to the current generation:\(error)")

}

NotificationCenter.default.addObserver(self, selector: #selector(type(of: self).storeRemoteChange(_:)), name: .NSPersistentStoreRemoteChange, object: nil)

// do {

// //try container.initializeCloudKitSchema(options: [.dryRun, .printSchema])

// try container.initializeCloudKitSchema()

// } catch {

// let nserror = error as NSError

// print("Could not initialize CloudKit schema: \(error.localizedDescription)")

// print(nserror.code)

// print(nserror.domain)

// }

return container

}()


Ands here's a function where I'm trying to toggle the CloudKit sync:


func enableiCloud(_ isEnabled: Bool) {

let cloudDescription = persistentContainer.persistentStoreDescriptions.first() {

$0.configuration == "Cloud"

}

guard cloudDescription != nil else { return }

if isEnabled {

cloudDescription!.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: cloudKitContainerIdentifier)

cloudDescription!.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)

} else {

cloudDescription!.cloudKitContainerOptions = nil

cloudDescription!.setOption(false as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)

}

}

What worked for me was setting cloudKitContainerOptions = nil before calling container.loadPersistentStores


lazy var persistentContainer: NSPersistentContainer = {

.

.

.

if !UserDefaultsManager.shared.syncWithCloudKit {

container.persistentStoreDescriptions.forEach { $0.cloudKitContainerOptions = nil }

}


container.loadPersistentStores( completionHandler: { (_, error) in ... } )


This is a hack since this works only when persistentContainer is first initialized.


Trying to do this after persistentContainer is initialized would require that I safely teardown and reinitialize the CoreDataStack. Reinitializing CoreDataStack creates a new managedObjectContext which then must be passed down the view hierarchy and to any services or managers that cache the managedObjectContext.


There are many reasons to want to toggle or pause syncing with CloudKit, and since NSPersistentCloudKitContainer is privy to all the inner workings of syncing with CloudKit, NSPersistentCloudKitContainer should be the one to handle this.



Some methods I could find useful would be:


enum SyncMode {

case disabled

case paused // Paus temporarily, useful for batch processing

case enabled

}


persistentContainer.setSyncMode( _ mode: SyncMode )


persistentContainer.syncAfter( someFutureTime ) // Sync after some time in the future.



Steve

Not sure you can feature-gate this since it's natively offered by Apple... using their iCloud services. What happens if you just remove the cloud descriptor?

I spoke to an Apple Engineer about this and they suggested the best way was to make your container return either an NSPersistentContainer or NSPersistentCloudKitContainer depending on whether you users have selected iCloud syncing internally in your app - you can use a UserDefaults bool to store this.


This works because NSPersistentCloudKitContainer is a subclass of NSPersistentContainer.


You will also need to set the NSPersistentHistoryTrackingKey to true on the vanillla container so changes are recorded should they switch iCloud back on. There doesn't appear to be any need to set any options manually on NSPersistentCloudKitContainer as they're enabled by default.


I have a PersistenceService class which manages my MOC and in it, this is how I set up the container:


    static var iCloudSyncData = UserDefaults.standard.bool(forKey: "iCloudSyncData")
    
    static var persistentContainer:  NSPersistentContainer  = {
        let container: NSPersistentContainer?
        if iCloudSyncData {
            container = NSPersistentCloudKitContainer(name: "MYAPP")
        } else {
            container = NSPersistentContainer(name: "MYAPP")
            let description = container!.persistentStoreDescriptions.first
            // This allows a 'non-iCloud' sycning container to keep track of changes if a user changes their mind
            // and turns it on.
            description?.setOption(true as NSNumber,
                                   forKey: NSPersistentHistoryTrackingKey)
        }
        container!.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                // Handle the errors
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        // As NSPersistentCloudKitContainer is a subclass of NSPersistentContainer, you can return either.
        return container!
    }()


This all works beautifully on my devices, and syncing (and toggling on/off) all works... but despite 'Deploying the Schema to Production' not one of my users in the 'real world' is able to sync their data with CloudKit. It seems like there is something wrong with the way their devices initially subscribe and I can't find an easy way to troubleshoot this as the API is somewhat opaque. I'm in discussions with an Apple Engineer about this so will hopefully have a solution soon.

It seems like there is something wrong with the way their devices initially subscribe and I can't find an easy way to troubleshoot this as the API is somewhat opaque. I'm in discussions with an Apple Engineer about this so will hopefully have a solution soon.

Please report your findings! I'm also having a similar issue!

Are there any news? Could they help you?

🙈🙉🙊

You know what would be really fantastic? If we had the option to choose which database to store the data in next to the checkbox for use CloudKit (public PLEASE!)
😉
I'm actually begging for this feature. Thanks in advance!

I have the same problem, when i used NSPersistentHistoryTrackingKey enable for non-cloud container, it auto sync my coredata to CloudKit. I have also tried setting the container's cloudKitContainerOptions property to nil but CloudKit sync still occur.

But when i not enable NSPersistentHistoryTrackingKey for non-cloud container , it give me error like,

Error : File is in Read Only mode due to Persistent History being detected but NSPersistentHistoryTrackingKey was not included.

Please explain me what is use of NSPersistentHistoryTrackingKey for cloud sync process.
Nitra's response will still cause this error in debug window, although not stop it from running


resetting internal state after error: Error Domain=NSCocoaErrorDomain Code=134410 "CloudKit setup failed because there is another instance of this persistent store actively syncing with CloudKit in this process."