Cannot add participants to CoreData to share between multiple users

I have been trying to get this to work since it was announced a few years ago but with no joy. I'm struggling to get Apple's example code to behave itself too. Seems overly complex and buggy. So I set out to create a simplified version myself. I have got the database to sync with CloudKit and I can see my records in the developer dashboard. I'm trying to use container.record(for: object.objectID) to get the CKRecord for it, but this always fails. The next step would be to add the participant.

I try to add the participant based on this code:

Button
{
    let record = fetchRecord(for: items[0]) //hack just to use the first record for dev testing
                    
    let share = CKShare(rootRecord: record)

    let persistenceController = PersistenceController.shared
                    
    persistenceController.addParticipant(
        emailAddress: "andrew@ambrit.com",
        permission: .readWrite,
        share: share) 
    { share, error in
        if let error = error
        {
            print("Error: \(error.localizedDescription)")
        }
        else if let share = share
        {
            print("Share updated successfully: \(share)")
        }
    }
} 
label:
{
    Label("Participants", systemImage: "person")
}

and

extension PersistenceController
{
    func addParticipant(emailAddress: String, permission: CKShare.ParticipantPermission = .readWrite, share: CKShare,
                        completionHandler: ((_ share: CKShare?, _ error: Error?) -> Void)?)
    {
        let container = PersistenceController.shared.container

        let lookupInfo = CKUserIdentity.LookupInfo(emailAddress: emailAddress)
        let persistentStore = privatePersistentStore //share.persistentStore!
        
        container.fetchParticipants(matching: [lookupInfo], into: persistentStore) { (results, error) in
            guard let participants = results, let participant = participants.first, error == nil else
            {
                completionHandler?(share, error)
                return
            }
            
            participant.permission = permission
            participant.role = .privateUser
            share.addParticipant(participant)
            
            container.persistUpdatedShare(share, in: persistentStore)
            { (share, error) in
                if let error = error
                {
                    print("\(#function): Failed to persist updated share: \(error)")
                }
                completionHandler?(share, error)
            }
        }
    }
}

My immediate problem is that when I call fetchRecord it doesn't find anything despite the record being available in the CloudKit dashboard.

func fetchRecord(for object: NSManagedObject) -> CKRecord
{
    let container = PersistenceController.shared.container
    print ("Fetching record \(object.objectID)")
    if let record = container.record(for: object.objectID)
    {
        print("CKRecord ID: \(record.recordID)")
        print("Record Name: \(record.recordID.recordName)")
        return record
    }
    else
    {
        fatalError("Record not found")
    }
}
Answered by DTS Engineer in 820981022

I'm struggling to get Apple's example code to behave itself too. Seems overly complex and buggy.

Would you mind to share what difficulty you have when trying the Apple sample code? When tackling an issue related to Core Data + CloudKit, I typically start with the Sharing Core Data objects between iCloud users sample. It is indeed a bit complicated, but is also a minimal project that demonstrates the techniques needed for implementing Core Data + CloudKit sharing.

Using the sample has the following benefits:

  • The Readme of the sample details the steps of how to configure and run the sample, with which you can run and play the sample without looking into the code.

  • If you see the sample has any problem, you can communicate with us without providing the details of your own implementation, and the communication will be quick because we know the sample.

  • You can use the sample as a test bed to try your code.

My immediate problem is that when I call fetchRecord it doesn't find anything despite the record being available in the CloudKit dashboard.

I'd use the sample as a test bed to check if your fetchRecord works. To do so, I found the Rate menu item in PhotoContextMenu.swift, and hooked your code to the action of the button, as shown below:

Button(action: {
    //activeSheet = .ratingView(photo)
    let container = PersistenceController.shared.persistentContainer
    print ("Fetching record \(photo.objectID)")
    if let record = container.record(for: photo.objectID) {
        print("CKRecord ID: \(record.recordID)")
        print("Record Name: \(record.recordID.recordName)")
    }
}) {
    MenuButtonLabel(title: "Rate", systemImage: "star")
}

I then did the following steps:

  1. Run the sample app on two devices (A and B) that are logged in with a samle iCloud account.
  2. Add a photo on device A, and observe the photo is synchronized to device B.
  3. On device A, right click the photo, and then click Rate menu.

At step 3, I observed the following output in my Xcode Console, meaning that record(for:) did find and return the CloudKit record:

Fetching record 0xb458b35aed5fa451 <x-coredata://FCBF4CF5-ADE4-446D-91E9-81B46C408D5E/Photo/p55> CKRecord ID: <CKRecordID: 0x600002b4b7c0; recordName=..., zoneID=com.apple.coredata.cloudkit.share....:defaultOwner> Record Name: ...

You can give it a try and see if you get the same result.

Note that step 2 is to be sure that the photo is synchronized to CloudKit. In the case where the photo isn't synchronized and hence the underlying CloudKit record doesn't exist yet, record(for:) returns nil, and I believe that was what happened in your case.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

I'm struggling to get Apple's example code to behave itself too. Seems overly complex and buggy.

Would you mind to share what difficulty you have when trying the Apple sample code? When tackling an issue related to Core Data + CloudKit, I typically start with the Sharing Core Data objects between iCloud users sample. It is indeed a bit complicated, but is also a minimal project that demonstrates the techniques needed for implementing Core Data + CloudKit sharing.

Using the sample has the following benefits:

  • The Readme of the sample details the steps of how to configure and run the sample, with which you can run and play the sample without looking into the code.

  • If you see the sample has any problem, you can communicate with us without providing the details of your own implementation, and the communication will be quick because we know the sample.

  • You can use the sample as a test bed to try your code.

My immediate problem is that when I call fetchRecord it doesn't find anything despite the record being available in the CloudKit dashboard.

I'd use the sample as a test bed to check if your fetchRecord works. To do so, I found the Rate menu item in PhotoContextMenu.swift, and hooked your code to the action of the button, as shown below:

Button(action: {
    //activeSheet = .ratingView(photo)
    let container = PersistenceController.shared.persistentContainer
    print ("Fetching record \(photo.objectID)")
    if let record = container.record(for: photo.objectID) {
        print("CKRecord ID: \(record.recordID)")
        print("Record Name: \(record.recordID.recordName)")
    }
}) {
    MenuButtonLabel(title: "Rate", systemImage: "star")
}

I then did the following steps:

  1. Run the sample app on two devices (A and B) that are logged in with a samle iCloud account.
  2. Add a photo on device A, and observe the photo is synchronized to device B.
  3. On device A, right click the photo, and then click Rate menu.

At step 3, I observed the following output in my Xcode Console, meaning that record(for:) did find and return the CloudKit record:

Fetching record 0xb458b35aed5fa451 <x-coredata://FCBF4CF5-ADE4-446D-91E9-81B46C408D5E/Photo/p55> CKRecord ID: <CKRecordID: 0x600002b4b7c0; recordName=..., zoneID=com.apple.coredata.cloudkit.share....:defaultOwner> Record Name: ...

You can give it a try and see if you get the same result.

Note that step 2 is to be sure that the photo is synchronized to CloudKit. In the case where the photo isn't synchronized and hence the underlying CloudKit record doesn't exist yet, record(for:) returns nil, and I believe that was what happened in your case.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Cannot add participants to CoreData to share between multiple users
 
 
Q