How to safely create root and branch objects in a custom zone?

I encountered issues with some branch objects being assigned to multiple zones error (on iOS 17.5.1 and above). I get the errors when calling persistentContainer.shareshare(:to:completion:) and persistentContainer.persistUpdatedShare(:in:completion:).

"The operation couldn't be completed. Request '89D3F62D-548D-4816-9F1B-594390BD8F70' was aborted because the mirroring delegate never successfully initialized due to error: Error Domain=NSCocoaErrorDomain Code=134060 "A Core Data error occurred." UserInfo={NSLocalizedFailureReason=Object graph corruption detected. Objects related to 'Oxa2255fdc1fa980c5 <x-coredata://CB800FA2-6054-4D91-8EBC-E9E31890344F/CDChildObject/p588>' are assigned to multiple zones: {l <CKRecordZonelD: 0x3026a1170; zoneName=com.apple.coredata.cloud-kit.share.5D30F204-5970-489F- BC2E-F863F1808A93, ownerName=defaultOwner>, <CKRecordZonelD: 0x302687b40; zoneName=com.apple.coredata.cloud-kit.zone, ownerName=_defaultOwner>"

In my setup, I moved all my root objects into one custom zone (there is only one custom zone in my private database). In one of my root object, there are 6 'one-to-one' and 2 'one-to-many' relationships. The branch objects can contains other relationships.

Create root object flow:

func saveToPersistent(_ object: ViewModelObject) {

    serialQueue.async {
        let context = backgroundContext()
        context.performAndWait {
            // Create new baby with its one-to-one child objects.
            let cdNewBaby = self.newCDBaby(object, context)
            if let share = self.getShareZone(.privateStore).first {

                self.moveToShareZone(pObjects, share: share, store: .privateStore)
            }
    CoreDataManager.single.saveContext(context)
            self.updateZoneNSaveContext([cdNewBaby], context: context)
        } // context.perform
    } // serialQueue.async
}

func backgroundContext() -> NSManagedObjectContext {
    
    let context = persistentContainer.newBackgroundContext()
    context.transactionAuthor = contextAuthor
    context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
    
    return context
}

func getShareZone(_ storeType: StoreType, zoneName: String? = nil) -> [CKShare] {
    
    var shares: [CKShare] = []
    do {
        shares = try persistentContainer.fetchShares(in: stores[storeType])
    } catch {
        print(error)
        return shares
    }
    
    if let zoneName = zoneName {

        shares = shares({ $0.recordID.zoneID.zoneName == zoneName })
    }
    return shares
}

func moveToShareZone(_ sharedObjects: [NSManagedObject], share: CKShare, store: StoreType) {

     self.persistentContainer.share(sharedObjects, to: share) { managedObjects, share, container, error in
        if let error = error {
            print(error)
        } else if let share = share, let store = self.stores[store] {
            self.persistentContainer.persistUpdatedShare(share, in: store) { (share, error) in
                if let error = error {
                    print(error)
                }
            }
        }
    }
    
} // moveToShareZone

Create one-to-many relationship branch object flow:

serialQueue.async {
        
    let context = self.backgroundContext()
        
    context.performAndWait {
        // MARK: Retrieve the Root record
        let pObjects = CDRootRecord.fetchRecord(rootRecord.uuidString, store: store, zoneName: zoneName, context: context)
                
        if let pRootRecord = pObjects.first {
            self.newCDLogContent(pRootRecord.self, viewModelObject: viewModelObject, context: context)
                // MARK: Save Log
                CoreDataManager.single.saveContext(context)
            }
        } // context.performAndWait
    } // serialQueue

Questions:

(1) Should I save a root object first before share to custom zone; or share to custom zone first before save? (I implemented save before share to zone in the past and found some issues on iOS16 where the object is not saved; and end of sharing object before save which works)

(2) As I understand, if a branch record is saved under a root record, it should automatically go into the root record. Or do I have to also share the branch record to the custom zone?

Answered by DTS Engineer in 796062022

First, I’d like to be clear about the behavior NSPersistentCloudKitContainer implements when you use it to share a managed object (NSManagedObject) so we are on the same page:

  1. When you save object A to the Core Data store associated with the private database, NSPersistentCloudKitContainer transforms the object to a CloudKit record A, and synchronizes it to the record zone 1 in the CloudKit private database, with zone ID being com.apple.coredata.cloudkit.zone.

  2. When you then use NSPersistentCloudKitContainer.share(_:to:completion:) to create a new share (CKShare) and share object A to another user, NSPersistentCloudKitContainer moves record A to a record zone 2, with zone ID being com.apple.coredata.cloudkit.share.<UUID> . Note that zone 2 is still in the private database.

  3. What you call NSPersistentCloudKitContainer.share(_:to:completion:) with an unsaved managed object A. Core Data saves the object for you, and the associated record A will be saved to zone 2.

Regarding Core Data relationships:

a. If you create object B and relate it to object A at step 1, NSPersistentCloudKitContainer will synchronize that to CloudKit record B in zone 1. Then at step 2, record B will be moved to zone 2.

b. If you create object B and relate it to object A after step 2, NSPersistentCloudKitContainer will synchronize object B to CloudKit record B directly to zone 2.

c. If you try to create another share (CKShare) to share object A or B after step 2, you will hit an error because the object A and B are already shared.

With the above, let's look into your questions:

(1) Should I save a root object first before share to custom zone; or share to custom zone first before save? (I implemented save before share to zone in the past and found some issues on iOS16 where the object is not saved; and end of sharing object before save which works)

Based on the behavior mentioned above, the approaches you described should both work.

(2) As I understand, if a branch record is saved under a root record, it should automatically go into the root record. Or do I have to also share the branch record to the custom zone?

I am not quite clear what exactly the root and branch records mean there, but when using NSPersistentCloudKitContainer, you work with Core Data objects, and don't need to work with CloudKit records in most of the cases, because Core Data takes care that for you.

If you mean the CloudKit records associated with the root Core Data object and its relationship, no, you don't need to share the relationship. Otherwise, you will hit an error, as mentioned in #c above.

You can observe all these behaviors by playing with the following sample project:

If you hit an issue, try to reproduce it with the above sample. If the sample does reproduce the issue, please share the steps and I'd take a closer look from there. Otherwise, you can compare your code with the sample to see what you do differently.

Regarding the error you are hitting, assuming that you don’t use CloudKit API directly to manipulate the CloudKit database, my best guess will be that a sharing flow failed or was interrupted in some way, which led to a data inconsistency. I unfortunately don't have a good suggestion for that case. The best thing I can see is that you detect and handle any error returned in the sharing flow, and clean up the objects.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Accepted Answer

First, I’d like to be clear about the behavior NSPersistentCloudKitContainer implements when you use it to share a managed object (NSManagedObject) so we are on the same page:

  1. When you save object A to the Core Data store associated with the private database, NSPersistentCloudKitContainer transforms the object to a CloudKit record A, and synchronizes it to the record zone 1 in the CloudKit private database, with zone ID being com.apple.coredata.cloudkit.zone.

  2. When you then use NSPersistentCloudKitContainer.share(_:to:completion:) to create a new share (CKShare) and share object A to another user, NSPersistentCloudKitContainer moves record A to a record zone 2, with zone ID being com.apple.coredata.cloudkit.share.<UUID> . Note that zone 2 is still in the private database.

  3. What you call NSPersistentCloudKitContainer.share(_:to:completion:) with an unsaved managed object A. Core Data saves the object for you, and the associated record A will be saved to zone 2.

Regarding Core Data relationships:

a. If you create object B and relate it to object A at step 1, NSPersistentCloudKitContainer will synchronize that to CloudKit record B in zone 1. Then at step 2, record B will be moved to zone 2.

b. If you create object B and relate it to object A after step 2, NSPersistentCloudKitContainer will synchronize object B to CloudKit record B directly to zone 2.

c. If you try to create another share (CKShare) to share object A or B after step 2, you will hit an error because the object A and B are already shared.

With the above, let's look into your questions:

(1) Should I save a root object first before share to custom zone; or share to custom zone first before save? (I implemented save before share to zone in the past and found some issues on iOS16 where the object is not saved; and end of sharing object before save which works)

Based on the behavior mentioned above, the approaches you described should both work.

(2) As I understand, if a branch record is saved under a root record, it should automatically go into the root record. Or do I have to also share the branch record to the custom zone?

I am not quite clear what exactly the root and branch records mean there, but when using NSPersistentCloudKitContainer, you work with Core Data objects, and don't need to work with CloudKit records in most of the cases, because Core Data takes care that for you.

If you mean the CloudKit records associated with the root Core Data object and its relationship, no, you don't need to share the relationship. Otherwise, you will hit an error, as mentioned in #c above.

You can observe all these behaviors by playing with the following sample project:

If you hit an issue, try to reproduce it with the above sample. If the sample does reproduce the issue, please share the steps and I'd take a closer look from there. Otherwise, you can compare your code with the sample to see what you do differently.

Regarding the error you are hitting, assuming that you don’t use CloudKit API directly to manipulate the CloudKit database, my best guess will be that a sharing flow failed or was interrupted in some way, which led to a data inconsistency. I unfortunately don't have a good suggestion for that case. The best thing I can see is that you detect and handle any error returned in the sharing flow, and clean up the objects.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

How to safely create root and branch objects in a custom zone?
 
 
Q