I have an app that uses NSPersistentCloudKitContainer
to synchronise data with iCloud. Users reported occasional data loss after which I started debugging my implementation.
Part of my app’s implementation is showing a pop-up that displays information and has a confirm button. When the user clicks this button, one or more instances of a custom NSManagedObject
subclass gets modified.
At the same time, NSPersistentCloudKitContainer
triggers a mirror process when the app resigns and becomes active. This happens when the user clicks the above mentioned confirm button. It does this by executing a NSCloudKitMirroringExportRequest
and NSCloudKitMirroringImportRequest
request.
In other words, in my app, data gets modified (explicitly via code) and gets mirrored (implicitly via CloudKit) at the same time, and this is where things go wrong.
Essentially this is what happens:
- A property (
NSTimeInterval
) of an instance of anNSManagedObject
subclass gets incremented (e.g. 0 + 1 = 1). NSPersistentCloudKitContainer
implicitly starts mirroring data when the app gets active or resigns.- During this process but before it finishes, the property gets incremented again (e.g. 1 + 1 = 2).
- Internally the mirroring process finishes.
At this stage I would expect the value of the property to match the latest (local) change (hence in my example 2). But instead, it matches the initial increment (hence in my example 1), overwriting my latest (local) change causing data loss. Apparently, the NSCloudKitMirroringImportRequest
reverts to the state of when the mirror process started, which also can be seen in the logs:
CoreData: debug: CoreData+CloudKit: -[PFCloudKitImporterZoneChangedWorkItem applyAccumulatedChanges:error:]
[…]
Importing updated records:
(
"<CKRecord: 0x10225b560; recordID=5458F9C8-7BE2-4563-92DE-650ED4C643F5:(com.apple.coredata.cloudkit.zone:__defaultOwner__), recordChangeTag=23, values={\n \"CD_activity\" = \"3E497A4C-6467-48F4-B7F4-6F1B2B7BC779\";\n \"CD_date\" = \"2020-04-26 07:28:00 +0000\";\n \"CD_duration\" = \"1\";\n \"CD_entityName\" = Mutation;\n}, recordType=CD_Mutation>"
)
At this stage I am not sure whether this is a bug in NSPersistentCloudKitContainer
or my app but it definitely results in unreliable data.
Currently I have these questions:
- Why does
NSPersistentCloudKitContainer
(specifically as a result ofNSCloudKitMirroringImportRequest
) imports “updated” records while those records have only changed locally? In other words: I already have the latest version of the records. Since those “updated” records have outdated values, which overwrites my newer changes, data loss is caused. Skipping these updates from happening would solve my issue. Is there any way to control this behaviour? - It seems like (explicitly) changing data and (implicitly) mirroring them at the same time causes issues. In my case this happens automatically due to
NSPersistentCloudKitContainer
scheduling the mirroring process when the app gets active or resigns. Is there any way of controlling this behaviour? Can I for example disable the mirroring process from happening when the app gets active or resigns? I noticedNSCloudKitMirroringDelegateOptions
defaulting to, amongst others,automaticallyScheduleImportAndExportOperations:YES
. Can I change this? And can I also trigger the mirroring process myself explicitly, at a moment when data mutation is less likely from happening?
Finally, a confirmation of the above can be found in the fact that when I am not connected to the Internet (hence, CloudKit cannot mirror data), things work as expected.