NSPersistentCloudKitContainer Bug

I have been excited to add NSPersistentCloudKitContainer's share functionality to my app but I've noted a few thing I suspect are bugs:

-When using share() on a NSManagedObject the share record is set up in a new zone. However, the root NSManagedObject's record is not being updated with the relationship linkage to the shared record.

-Secondly, when you revoke a share, the cloudkit.share record is removed from iCloud, but not in the local data stores. This makes the fetchShares() method ineffective for detecting a missing cloudkit.share record. In order to re-share the root object the developer must call out to iCloud directly using the old methods to be sure if the share exists or not.

I am using the code from Apple's 'Synchronizing a Local Store to the Cloud' sample. It would be nice if they added support for revoking shares into this sample and addressed these issues.

I'm struggling with these issues now too.

  • Currently Core Data with CloudKit backs up all NSManagedObjects corresponding to CKRecord as well as CKShare for efficiency reasons, which should be done locally. As a result, even if the developers modify the CKShare data by code, they still can't get the local ckshare catch updated. This makes it difficult for developers to design their own UICloudShareingController.

  • Currently the local ckshare is not updated after stopping sharing using the UICloudSharingController, resulting in the ckshare effectively being disabled. This results in the UICloudSharingController not being able to use it for initialisation. I tried deleting the customzone on the server via code, and after deletion, the shared customzone would continue to be restored after the next app cold start due to the local catch mechanism. ideally, the data should be moved from the customzone back to the private customzone after the owner stops sharing, com.apple.coredata .cloudkit.zone, and update the local catch to remove the CKShare corresponding to the shared NSManageObjct

  • There is no notification mechanism available on the participant side when an owner pair disables a participant share. The current situation is that the share record on the participant's device is not working well at this point, and when the user clicks on it, it crashes the application. I would like to be able to get an alert on the participant side that the share has been stopped, so that the code can easily handle it.

  • After the participant stops sharing of their own accord, the local ckshare is refreshed but does not disappear and the shared data disappears the next time the app is cold-launched, but if the app is not cold-launched and the user clicks on the shared data, it will cause the program to crash. My current solution is to delete the local NSManagedObject for that share in the callback method

Does Apple read these forums? Or is there a better place to file bugs for their Beta software? It would be nice if we could understand if this was an acknowledged issue with the new release of NSPersistentCloudKitContainer.

I have submitted a Feedback to Apple. Updates to ckshare in the cache can currently be resolved by persistUpdatedShare. The issue of deleting custom Zones after stopping a share is solved by purgeObjectsAndRecordsInZone. There is still a lot of work to be done on your own though. Also, the UICloudSharingController still does not automatically use the above methods.

I have written a small demo https://github.com/fatbobman/ShareData_Demo_For_CoreDataWithCloudKit

I wanted to follow up on this statement in my first post for any other readers: "the root NSManagedObject's record is not being updated with the relationship linkage to the shared record."

I figured out later that this is because there are 2 different ways to do shares: you share Records or Zones. The new share functionality in NSPersistentCloudKitContainer appears to be creating full Zone shares unlike the code documented on CKShare. When the Zone share is made there are no relationships created on the records in the zone because they are all made available to the Participants.

In terms of the bug with local ckshare being updated, that is still an issue. I haven't tested the suggestion by Fat Xu yet and in the meantime I had to query CloudKit directly (not thru NSPersistentCloudKitContainer) for the most recent ckshare records.

I've discovered a workaround to the issue of the local cache not being updated when the owner stops sharing. At first, I tried Fat Xu's method of first making a copy of the record and then using purgeObjectsAndRecordsInZone (essentially resetting things to the way they were before sharing.) The downside to this method is that the participants are not notified of the change, as Fat Xu pointed out.

However,

If, instead of using purgeObjectsAndRecordsInZone, you use a simple context.delete() function to delete the record (after making a copy), then the participants do get notified. There must be something about purging the zones that screws up the participant's effort to maintain sync.

The only remaining issue is that you'll have empty share zones in your container. I chose to solve this by running a background task upon startup to delete all empty zones that were set up for sharing. It's working for me so far, so I figured I'd let you guys know about it.

Some apple engineer has recently published a demo that I think is well worth checking out. It will answer most of the questions asked here: https://github.com/ziqiaochen/CoreDataCloudKitShare

For the empty zone cleanup I did the following. (For context see my post above. It wouldn't let me paste code blocks in my reply otherwise)

In the init of my CoreDataStack, I added this:

// Perform async function to clean up unused share zones leftover from deletions
        Task {
            do {
                try await performZoneCleanup()
            } catch {
                print("*** Error in performZoneCleanup: ", error)
            }
        }

And then added this function:

func performZoneCleanup() async throws {
        // Get list of shared zones
        let container = CKContainer.default()
        let privateDB = container.privateCloudDatabase
        let zones = try await privateDB.allRecordZones()
        
        // Filter to keep empty ones
        for zone in zones {
            let query = CKQuery(recordType: "CD_StudentSubjectMO", predicate: NSPredicate(value: true)) // Adjust record type for your model
            let records = try await privateDB.records(matching: query, inZoneWith: zone.zoneID, desiredKeys: nil, resultsLimit: 100)
            if records.matchResults.isEmpty && zone.zoneID.zoneName != CKRecordZone.ID.defaultZoneName {
                let zoneID = try await persistentContainer.purgeObjectsAndRecordsInZone(with: zone.zoneID, in: privatePersistentStore)
                print("ZoneID purged: \(zoneID)")
            }
        }

        print("CoreDataStack has completed empty zone cleanup")
}

Note: You may get some console warnings from NSCloudKitMirroringDelegate saying it can't find the zones you purged, especially on other devices, but I don't see a way around that until Apple cleans this process up for us.

Sorry everyone for garbaging this thread a bit, but I figured out one thorny issue with this whole thing. It's regarding the situation when an owner stops sharing a record.

So, to recap, Fat Xu discovered using purgeObjectsAndRecordsInZone (after making a deep copy) does work to keep the owner's database correct. I was having the issue that the other participants were not getting notification of this change. That was why I went down the road of using viewContext.delete, and then later cleaning up the empty zones so that the change would be received by recipients and the owner wouldn't have a bunch of empty zones building up over time.

I finally discovered why my participants were not getting the notification. It was a bug in Apple's sample app for this. (10015: Build Apps that Share Data Through CloudKit and Core Data). I had used this sample app as a basis for processing remote store notifications.

In that code, after fetching the NSPersistentHistoryTransactions, it checks to see if the results are NSPersistentHistoryResult AND is not empty. That last bit was the problem. I tried searching for documentation that would explain the NSPersistentHistoryResults more, but unfortunately, Apple did not give us much to go by. It turns out that when it's processing the scenario where the owner of a share purges the zone, the result looks different than if it were other types of transactions. The NSPersistentHistoryResult.result is empty, so my code (again, based on the sample code) ignored it.

Once I removed that check for transactions being empty, it correctly passed the notification to my UI that the record is no longer shared and should be deleted.

So, disregard my previous answer with the code to delete unused zones. Just use FatXu's method of making a deep copy, then purging the zone, and as long as you're processing the notifications and persistent history correctly (not using Apple's sample code), then it will work for you.

Cheers!

NSPersistentCloudKitContainer Bug
 
 
Q