How is Record Zone Sharing done?

1.8k views Asked by At

My use case is the following: Every user of my app can create as an owner a set of items. These items are private until the owner invites other users to share all of them as participant. Participants can modify the shared items and/or add other items.
So, sharing is not done related to individual items, but to all items of an owner.

I want to use CoreData & CloudKit to do CoreData/CloudKit mirroring and to have local copies of private and shared items. To my understanding, CoreData & CloudKit puts all mirrored items in the private database in a special zone „com.apple.coredata.cloudkit.zone“. So, this zone should be shared, i.e. all items in it.

In the WWDC 2021 video „Build apps that share data through CloudKit and Core Data“ it is said that NSPersistentCloudKitContainer uses Record Zone Sharing optionally in contrast to hierarchically record sharing using a root record. In the docs to the UICloudSharingController an example is given how to share records using a rootRecord. I assume this can be modified to use a shared record zone instead.
The shareRecord e.g. could be initiated instead with

let shareRecord = CKShare(rootRecord: rootRecord)

with

let shareRecord = CKShare.init(recordZoneID:)  

But is this the ID of the record zone that uses Apple for mirroring, i.e. „com.apple.coredata.cloudkit.zone“? And if so, how do I get its CKRecordZone.ID, if I have only the zone name?

1

There are 1 answers

6
Reinhard Männer On BEST ANSWER

By now I figured out how this can be done. Maybe it helps somebody else.

It starts with tapping a share button.
First, it is checked if a CKShare record already exists in the iCloud private database. Depending on it, a UICloudSharingController is initialized either so that it created it, or so that it uses it. If it should create the share, it is configured accordingly.

// See <https://developer.apple.com/documentation/uikit/uicloudsharingcontroller>
@IBAction func sharingButtonTapped(_ sender: UIBarButtonItem) {
    let barButtonItem = sender
    
    /*
     If no CKShare record has been stored yet in iCloud, it will be created below using UICloudSharingController initialized with a preparation handler.
     If it exists already, UICloudSharingController is initializes with the existing CKShare record.
     */
    getShareRecord { result in
        DispatchQueue.main.async {
            let cloudSharingController: UICloudSharingController!
            switch result {
            case .success(let ckShareRecord):
                if let shareRecord = ckShareRecord {
                    cloudSharingController = UICloudSharingController.init(share: shareRecord, container: CKContainer.default())
                } else {
                    cloudSharingController = UICloudSharingController { [weak self] (controller, completion: @escaping (CKShare?, CKContainer?, Error?) -> Void) in
                        guard let `self` = self else { return }
                        self.share(completion: completion)
                    }
                }
                cloudSharingController.delegate = self
                
                if let popover = cloudSharingController.popoverPresentationController {
                    popover.barButtonItem = barButtonItem
                }
                self.present(cloudSharingController, animated: true) {}
            case .failure(let error):
                fatalError("\(error)")
            }
        }
    }
}

// For sharing see https://developer.apple.com/documentation/cloudkit/shared_records
private func share(completion: @escaping (CKShare?, CKContainer?, Error?) -> Void) {
    if #available(iOS 15, *) {
        // iOS 15++
        let recordZoneID = CKRecordZone.ID(zoneName: "com.apple.coredata.cloudkit.zone", ownerName: CKCurrentUserDefaultName)
        let shareRecord = CKShare(recordZoneID: recordZoneID)
        
        // Configure the share so the system displays the shopping lists name and logo 
        // when the user initiates sharing or accepts an invitation to participate.
        CloudKitHelper.fullNameOfICloudAccount { result in
            var fullName: String!
            switch result {
            case .success(let name):
                fullName = name
            case .failure(let error):
                print("Could not read user name: \(error)")
                fullName = "Unknown user"
            }
            shareRecord[CKShare.SystemFieldKey.title] = String(format: NSLocalizedString("SHARING_LIST_OF_USER", comment:" "), fullName)
            let image = UIImage(named: kFileNameLogo)!.pngData()
            shareRecord[CKShare.SystemFieldKey.thumbnailImageData] = image
            // Include a custom UTI that describes the share's content.
            shareRecord[CKShare.SystemFieldKey.shareType] = "com.zeh4soft.ShopEasy.shoppingList"
            
            let recordsToSave = [shareRecord]
            let container = CKContainer.default()
            let privateDatabase = container.privateCloudDatabase
            let operation = CKModifyRecordsOperation(recordsToSave: recordsToSave, recordIDsToDelete: nil) 
            operation.perRecordProgressBlock = { (record, progress) in
                if progress < 1.0 {
                    print("CloudKit error: Could not save record completely")
                }
            }
            
            operation.modifyRecordsResultBlock = { result in
                switch result {
                case .success:
                    completion(shareRecord, container, nil)
                case .failure(let error):
                    completion(nil, nil, error)
                }
            }
            
            privateDatabase.add(operation)
        }
        
    } else {
        // iOS <15
        fatalError("Sharing is only available in iOS 15")
    }
}

func getShareRecord(completion: @escaping (Result<CKShare?, Error>) -> Void) {
    let query = CKQuery(recordType: "cloudkit.share", predicate: NSPredicate(value: true))
    privateDB.fetch(withQuery: query) { result in
        switch result {
        case .success(let returned): 
            // .success((matchResults: [CKRecord.ID : Result<CKRecord, Error>], queryCursor: CKQueryOperation.Cursor?))
            let matchResults = returned.0 // [CKRecord.ID : Result<CKRecord, Error>]
            switch matchResults.count {
            case 0:
                completion(.success(nil))
                return
            case 1:
                let recordResult = matchResults.values.first!
                switch recordResult {
                case .success(let ckRecord):
                    completion(.success(ckRecord as? CKShare))
                    return
                case .failure(let error):
                    completion(.failure(error))
                    return
                }
            default:
                fatalError("More than 1 CKShare record")
            }
        case .failure(let error):
            completion(.failure(error))
            return
        }
    }
}