NSPersistentCloudKitContainer reverts NSManagedObject subclass to historic value
Asked Answered
C

0

7

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:

  1. A property (NSTimeInterval) of an instance of an NSManagedObject subclass gets incremented (e.g. 0 + 1 = 1).
  2. NSPersistentCloudKitContainer implicitly starts mirroring data when the app gets active or resigns.
  3. During this process but before it finishes, the property gets incremented again (e.g. 1 + 1 = 2).
  4. 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:

  1. Why does NSPersistentCloudKitContainer (specifically as a result of NSCloudKitMirroringImportRequest) 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?
  2. 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 noticed NSCloudKitMirroringDelegateOptions 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.

Countermand answered 24/4, 2020 at 11:49 Comment(7)
I have a very same issue. Some cases data from CloudKit overrides local updated data.Blintze
I requested technical support from Apple to look into this. They advised me to report a bug as they believed this might be a bug in CloudKit. I also noticed this is happening with other entities within my app. So far I did not receive any feedback from the Apple engineers. As a workaround I have overridden related setters of entities and avoid setting the primitive value when I notice the "new" value (coming from CloudKit) actually represents an older value than the one currently (locally) set.Countermand
Did you find a solution other than the workaround?Torpid
@NielsMouthaan could you please also elaborate on how you implemented the workaround?Torpid
@BenjaminAngeria unfortunately this still seems a bug in macOS, although I haven't tested this on Big Sur yet. The workaround is very specific to my app, so not sure if it really helps you. But see my comment above for some inspiration.Countermand
@NielsMouthaan I'm having similar problems on iOS 13-14 also. I'm specifically wondering how to prevent an old value to overwrite the newest value. How did you override the setters and look at change date? Could you point to any docs or resources? Can't seem to find anything of help...Torpid
I have just overridden the setters of my managed object subclasses. In those setters I check whether the change represents something I did myself (hence, is expected) or is caused by CloudKit (in my case unexpected as I never change values for those objects, I only set them once). That last piece is why this workaround works for my case: if you expect objects (and specifically their properties) to be changed (by other systems) this won't work.Countermand

© 2022 - 2024 — McMap. All rights reserved.