NSPersistentCloudKitContainer losing data

Some users of my app are reporting total loss of data while using the app. This is happening specifically when they enable iCloud sync.

I am doing following


private func setupContainer(enableICloud: Bool) {
    container = NSPersistentCloudKitContainer(name: "")
    container.viewContext.automaticallyMergesChangesFromParent = true
    container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy

    guard let description: NSPersistentStoreDescription = container.persistentStoreDescriptions.first else {
        fatalError()
    }

    description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
    description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)

    if enableICloud == false {
        description.cloudKitContainerOptions = nil
    }

    container.loadPersistentStores { description, error in
        if let error {
            // Handle error
        }
    }
}

When user clicks on Toggle to enable/disable iCloud sync I just set the description.cloudKitContainerOptions to nil and then user is asked to restart the app.

Apart from that I periodically run the clear history

func deleteTransactionHistory() {
    let sevenDaysAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date())!
    let purgeHistoryRequest = NSPersistentHistoryChangeRequest.deleteHistory(before: sevenDaysAgo)
    let backgroundContext = container.newBackgroundContext()
    backgroundContext.performAndWait {
        try! backgroundContext.execute(purgeHistoryRequest)
    }
}

I see folks implement an in-app toggle to enable / disable CloudKit synchronization when using NSPersistentCloudKitContainer. I honestly don't see that a great idea. I know this is controversial, and so would layout my reasoning here:

First, the system already provides a setting (Settings > Apple ID > iCloud) that allows users to turn on / off iCloud for an app. If a user turns off iCloud for an app with the setting, the app won’t be able to synchronize with CloudKit, even the in-app toggle is on.

Secondly, for the toggle to work, the app needs to release the Core Data objects currently in use, and reload the Core Data stack. In your case, you achieve that by asking the user to restart the app, which is probably not a great experience.

Last but probably more importantly, CloudKit implements access control on its data: only the store owner (the user who owns the login iCloud account when the store is created) can access a store associated with a CloudKit private or shared database, and only the data owner (the user who owns the login iCloud account when the data is created) can modify the data in a store associated with a CloudKit public database by default.

When a Core Data store is tied to a CloudKit database, NSPersistentCloudKitContainer enforces the access control. For example, it wipes off the Core Data store on the device when it gets notified that the user logs out iCloud. (This doesn’t lead to data loss, because NSPersistentCloudKitContainer downloads the data from CloudKit when the user logs back in with the same iCloud account.) Using NSPersistentContainer, or NSPersistentCloudKitContainer with cloudKitContainerOptions being set to nil, to manage an existing Core Data + CloudKit store loses the access control, and may introduce data privacy and security concerns.

To be more concrete, consider the following flow:

a. A user uses the in-app toggle to turn off CloudKit synchronization. As a result, the app sets cloudKitContainerOptions to nil and reload the store, and the store isn't tied to a CloudKit database anymore.

b. The current user logs out the device.

c. A new user launches the app and add more data.

d. The original user logs in again and toggles CloudKit synchronization back on, which reloads the store with cloudKitContainerOptions being set to a valid value.

At step c, NSPersistentCloudKitContainer doesn't enforce the access control, and so the existing data spreads to the new user.

At step d, it is unclear to the system who owns the data added at step c and how to handle it. I believe the behavior today is that NSPersistentCloudKitContainer resumes the synchronization, assuming that the data belongs to the orignal user, which may or may not be right.

Having said that, given you have implemented the in-app toggle, I'd like to comment a bit on your immediate question:

"Some users of my app are reporting total loss of data while using the app. This is happening specifically when they enable iCloud sync."
...
"Apart from that I periodically run the clear history"

This may be the culprit. NSPersistentCloudKitContainer relies on the history to figure out the changes on the store since last export. If the history is purged before being processed by NSPersistentCloudKitContainer, the changes happened the local device won't be synchronized to CloudKit.

It is typically no harm to leave the history there, but if you do need to purge it because the size of your store grows to very large, consider following the suggestion mentioned in the last paragraph in the following article:

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Thank you for answering in detail. I have following questions

  1. Are you suggesting that iCloud-sync should be enabled by default and if the users want to turn it off then they can do that from System Settings app?

  2. Based on your answer, I think I agree that having toggle to enable/disable iCloud will not provide consistent experience. But then I don't understand how that can be fixed since iCloud is available only to the premium users in the app. The toggle is off and disabled for the free users of the app.

  3. Is there a way to know if the user has disabled iCloud for the app from System Settings?


c. A new user launches the app and add more data.

I am sorry, I did not follow this part where data of 2 users get mixed. Won't the new user have a separate container where their data will be separately stored and if they have logged into their iCloud account then it will sync in their account?

"1. Are you suggesting that iCloud-sync should be enabled by default and if the users want to turn it off then they can do that from System Settings app?"

Yeah, that will be my choice.

"2. Based on your answer, I think I agree that having toggle to enable/disable iCloud will not provide consistent experience. But then I don't understand how that can be fixed since iCloud is available only to the premium users in the app. The toggle is off and disabled for the free users of the app."

I didn't think that you'd build your business model based on that. Thanks for bringing this up.

I haven't thought from the business model perspective, but one idea is to consider creating a local store and a CloudKit store to manage the data separately. When users choose to opt in, you move the data from local store to the CloudKit one, and let NSPersistentCloudKitContainer take care the rest. By using a local store, you control when to move what data to the CloudKit store.

For details about managing multiple stores with one Core Data stack, see Linking Data Between Two Core Data Stores: <https://developer.apple.com/documentation/coredata/linking_data_between_two_core_data_stores>

(The sample doesn't exactly demonstrate a CloudKit store, but the idea of managing multiple stores with one Core Data stack applies.)

"3. Is there a way to know if the user has disabled iCloud for the app from System Settings?"

You can use accountStatus(completionHandler:) to determines whether the system can access the user’s iCloud account.

iCloud is unavailable to your app if:

  • The device is not logged in with an Apple account, or
  • The device is logged in but the iCloud setting for your app is disabled.

"c. A new user launches the app and add more data. I am sorry, I did not follow this part where data of 2 users get mixed. Won't the new user have a separate container where their data will be separately stored and if they have logged into their iCloud account then it will sync in their account?"

That is because step #a turns off CloudKit synchronization by setting cloudKitContainerOptions to nil, and so NSPersistentCloudKitContainer treats the store as a non-CloudKit store, and doesn't handle the account switching.

It has been a long while since the last time I examined the behavior and I am not very sure if the behavior has changed, but that is the theoretical behavior based on my understanding on how NSPersistentCloudKitContainer works. You can verify the behavior with your own testing.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

NSPersistentCloudKitContainer losing data
 
 
Q