5

I use NSPersistentCloudKitContainer to save objects in CoreData + CloudKit. I have integrated a sharing function that moves an object to a separate zone to share using UICloudSharingController, as described in https://developer.apple.com/wwdc21/10015

When the user stops sharing, I want the object in the shared zone to be deleted, and moved back to the CoreData + CloudKit standard private zone. Deleting the CKShare and its zone is done using the following method:

/**
 Delete the Core Data objects and the records in the CloudKit record zone associcated with the share.
 */
func purgeObjectsAndRecords(with share: CKShare, in persistentStore: NSPersistentStore? = nil) {
    guard let store = (persistentStore ?? share.persistentStore) else {
        print("\(#function): Failed to find the persistent store for share. \(share))")
        return
    }
  
    persistentContainer.purgeObjectsAndRecordsInZone(with: share.recordID.zoneID, in: store) { (zoneID, error) in
        if let error = error {
            print("\(#function): Failed to purge objects and records: \(error)")
        }
    }
}

How do I deep copy the CKShare back to the private zone before deleting it?

  • Did my answer solve your problem? If not, what is missing? – Reinhard Männer Apr 09 '22 at 19:39
  • Hi Reinhard, thanks for your answer and sorry for not commenting. I had commented something, but it appears it wasn't saved or posted properly. I will rewrite the comment on your answer soon. – FrontFacingWindowCleaner Apr 11 '22 at 07:04
  • Hi, did you solve your problem? I am interested if I missed something in my implementation. – Reinhard Männer May 02 '22 at 11:08
  • I ended up implementing a deep copy solution. So just before deleting, I create new objects for all the about to be deleted objects. Then I delete. The user doesn't really notice that everything is a copy, so it works okay. It's just not very elegant... – FrontFacingWindowCleaner May 02 '22 at 14:30
  • Good to know, but I still don't understand why you need a deep copy; I don't need one. Maybe because you have relations in your CoreData model? I don't. – Reinhard Männer May 02 '22 at 18:40
  • I have a bunch of relations, yeah. When I have some time I might build a little demo app from the ground up to see whether I can get it to work like it should. I’ll let you know. – FrontFacingWindowCleaner May 03 '22 at 11:18

1 Answers1

1

I am not sure if I understand you right, but I will try an answer:

Assume a record hierarchy or a zone in the private database should be shared by the owner.
When sharing is initiated a user is invited to share the data, and a CKShare record is initialized in the owner's private database.
When a user accepts the invitation, the CKShare record and the shared data are made accessible for the user via the user's shared database. They are not copied to the shared database; the shared database is just a window to the private database of the owner. However if the shared database is mirrored by CoreData + CloudKit to a persistent store of the share user, NSManagedObjects are created for the shared data.

When the owner or the user stops sharing, these NSManagedObjects must normally no longer be accessible by the user. In principle this is also handled by iCloud mirroring: The shared database is no longer a window to the owner's data, i.e. it no longer contains the CKShare record and the shared data, and thus mirroring deletes them from the user's persistent store. But this may take long. To delete the local copies of the shared data faster, one can call persistentContainer.purgeObjectsAndRecordsInZone.

Now to your question that I do not understand: What do you mean by "deep copy the CKShare back to the private zone"? The owner's private database has never been modified (except from updating the user's status in the CKShare record or - when sharing stopped for the last user - the deletion of the CKShare record). So there is no need to copy back the CKShare record. The user's private database has neither been modified.
The only situation where a "deep copy" back to the private database makes sense to me is when the share user wants to keep the shared data even after sharing stopped. If you want to do this, you had to copy all shared objects as soon as they become available, i.e. as soon as they are mirrored from the iCloud shared database to the local persistent store. You could use a .NSPersistentStoreRemoteChange notification to do the copying. purgeObjectsAndRecordsInZone would then only delete the originals, not the copies.

EDIT:

Let's take an example:
A user, called here "owner", has some owner records in a CoreData persistent store that is mirrored to iCloud to the owner's private database. During setup, iOS creates in the owner's private database a new zone "com.apple.coredata.cloudkit.zone".

Assume first that no records are shared.
Then iOS will update the persistent store of all devices logged in to the same iCloud account:
Local changes are exported to this zone in the owner's private database, and iCloud changes are imported to the owner's persistent store.

Now assume that the owner invites another user, called here "participant", to share either an owner's record hierarchy or an owner's zone.
Then, a CKShare record is created in the owner's private database that specifies the sharing details, i.e. what is shared by whome.

The participant, who has the same app, has some participant's records in the participant's persistent store that is mirrored to the participant's private database. During setup, iOS creates in the participant's private database a new zone "com.apple.coredata.cloudkit.zone".

When the participant accepts the owner's invitation to share data, iOS maps the owner's shared data to the participant's shared database. "Mapping" means that the owner's data that are in the com.apple.coredata.cloudkit.zone zone in the owner's private database appear now in the participant's shared database in a new zone "com.apple.coredata.cloudkit.zone". Together with the shared data, the CKShare record of the owner's private database is also mapped to the participant's shared database.
This zone is now mirrored by iOS to the participant's persistent store.
For the owner, nothing has changed except the CKShare record.

When the owner or the participant stops sharing, the mapping of the owner's data in the owner's private database is terminated, i.e. the owner's shared data no longer appear in the participant's shared database.
Since they are deleted for the participant (but not for the owner), this is mirrored to the participant's persistent store and the shared records are deleted in the participant's persistent store. However, this takes a while. In order to delete the shared data immediately, one can use persistentContainer.purgeObjectsAndRecordsInZone when sharing is terminated.

I hope this clarified the situation!

Reinhard Männer
  • 14,022
  • 5
  • 54
  • 116
  • 1
    When you create a new CKShare, the associated records are moved from a regular zone, to a newly created zone. This zone is then shared to other people's shared database when they accept. If the owner stops sharing, the records remain in this shared zone (in their private database). "The only situation where a "deep copy" [...] wants to keep the shared data even after sharing stopped." Maybe I am misunderstanding Apple's use case of shared databases, because doesn't it make sense for the owner to want to keep a copy of their soon-to-be-stopped-sharing records? – FrontFacingWindowCleaner Apr 11 '22 at 08:19
  • Functionality remains after the Owner removes the share functionality, but the records remain in their own shared zone. I can imagine that over a long use case with an app, your CloudKit becomes a gigantic mess with old leftover zones. Hence I would like to move all records back to the default state after sharing stops. – FrontFacingWindowCleaner Apr 11 '22 at 08:20
  • When a new CKShare is created by an owner in the private database, no record is moved. The shared record hierarchy/zone is mapped into the shared database of the sharing participant. The sharing participant can access the shared record hierarchy/zone in his/her shared database, i.e. the original records in their original place are accessed, not copies. Therefore, the problem that you see does not arise at all. When sharing is stopped, the window to the original records is closed for the sharing participant, but for the owner, nothing has changed (besides the CKShare record). – Reinhard Männer Apr 11 '22 at 09:36
  • You can find a tutorial [here](https://medium.com/@adammillers/cksharing-step-by-step-33800c8950d2) – Reinhard Männer Apr 11 '22 at 09:44
  • "When sharing is stopped, the window to the original records is closed for the sharing participant, but for the owner, nothing has changed (besides the CKShare record)." except that the records remain in the shared zone, right? If you share and unshare a bunch of records, you'll end up with a very messy CloudKit with each record in their own zone, rather than in the default zone? – FrontFacingWindowCleaner Apr 12 '22 at 09:53
  • I will edit my answer to make it more clear. But this will take a little... – Reinhard Männer Apr 12 '22 at 09:57
  • Thanks so much for your edit and taking the time to help me. ""Mapping" means that the owner's data that are in the com.apple.coredata.cloudkit.zone zone in the owner's private database appear now in the participant's shared database in a new zone "com.apple.coredata.cloudkit.zone"." But what if we want to share only certain records in our cloudkit.zone? Then we need to move the records to a separate zone, because how else would CloudKit know which records to share with who? – FrontFacingWindowCleaner Apr 14 '22 at 15:34
  • Or is it that just for some reason, my code creates a new zone for each shared records and this is not expected behavior at all? – FrontFacingWindowCleaner Apr 14 '22 at 15:43
  • My code uses zone sharing, not record hierarchy sharing. Thus, I cannot prove the following. But I believe it does not matter how many zones are created by sharing - they all are simply a window from the participant to the owner. And if you close the window by stopping the sharing, these zones with their mapped records just disappear. Did you check what happens to these records after sharing stop? – Reinhard Männer Apr 14 '22 at 16:14
  • The main source for code on SwiftUI + CloudKit Sharing is this one project: https://github.com/delawaremathguy/CoreDataCloudKitShare/blob/5a1894c60591c7005905f956a11a58eada1a92ec/Persistence/PersistenceController%2BShare.swift If you scroll down to func cloudSharingControllerDidStopSharing(_ csc: UICloudSharingController), you'll see the comments: – FrontFacingWindowCleaner Apr 15 '22 at 09:10
  • "Purging the zone a caveat: - When sharing an object from the owner side, Core Data moves the object to the shared zone; - When calling purgeObjectsAndRecordsInZone, Core Data removes all the objects and records in the zone. To keep the objects, deep copy the object graph you would like to keep and relate it to an unshared object (relationship)." To me it shows that it is intended functionality that all the objects are removed after stopping the share. I have not encountered any way in my code that the zones just revert back to a normal record. Everything is deleted – FrontFacingWindowCleaner Apr 15 '22 at 09:11
  • I am sorry, I cannot help. In my app, it works exactly as I did describe it. – Reinhard Männer Apr 15 '22 at 09:42
  • It must be my code, I will tear it down and rewrite to see whether it solves anything. If I figure out what's happening, I'll let you know. Thanks a lot so far for your time. – FrontFacingWindowCleaner Apr 15 '22 at 11:34
  • 1
    Updates? I also find when a participant removes themselves from a share, the Core Data entity (and hierarchy) is deleted from Core Data locally. (True for my app, and Apple's sample app in gitHub: https://github.com/apple/sample-cloudkit-sharing). Is this correct behavior?? How does one persist a CD entity/children after a participant leaves the share, or owner stops sharing? It seems to me an 'Owner' that creates a list should still have access to the list/children after shares are removed (as in the case when using Apple's Notes, Reminders, Shared photo albums etc..). – DIV Dec 06 '22 at 15:18
  • @ReinhardMänner I believe the different behaviours you and FrontFacing are describing result from two different ways you are implementing sharing. Reinhard seems to do rootRecord sharing, versus FrontFacing is doing a zoneSharing through the new sharing capability of NSPersistentCloudKitContainer. You both are right in what you describe, and FrontFacing needs a deep copy. See: https://developer.apple.com/documentation/coredata/sharing_core_data_objects_between_icloud_users (scroll down to 'Share a Core Data object' last paragraph) – KlausM Jan 20 '23 at 13:59