CloudKit Sharing: Five Tips and Tricks

CloudKit

 

While I use Ensembles to manage the Core Data iCloud syncing process (though that may be changing…), I’ve also began to use CloudKit to drive portions of Caramel. I am just about finished using it to power the shared list feature, which allows a single person to share a list with others who can then make edits to the items in it, and have also began to make use of CloudKit’s public database to push changes to the default items included in everyone’s copy.

Along the way, I noticed there’s a few important tricks about CloudKit sharing that are fragmented or learn-as-you-go, so I’m hoping that I can help by collecting and showing examples of five of those things here.

 

 

An accepted CKShare is a lens into a private database

The sharedCloudDatabase is just a collection of zones which have records that truly exist in the sharer’s privateCloudDatabase. When User B interacts with these records, they are modifying or viewing the original records in User A’s space. A CKShare creates the reference to this record and also explains the permission level based on who is accessing it.

Because I use CloudKit as a secondary storage mechanism to Core Data whose purpose is primarily to notify changes across devices, it helped to create a distinction between Master and Child since each party has a different responsibility.

 

Sharing requires using custom zones

In order to save a CKShare, you can’t use the Default Zone in the user’s privateCloudDatabase to store the share or any attached records. Instead, you have to use a custom zone (and that zone is actually what is shared to another user’s sharedCloudDatabase). Here is a quick and dirty way to check for and make a custom zone:

let sharedListZone = CKRecordZone(zoneName: "SharedZone")
let container = CKContainer.default()

func setupZones(completion: @escaping (Error?) -> Void) {
    container.privateCloudDatabase.fetch(withRecordZoneID: sharedListZone.zoneID, completionHandler: { zone, error in
        guard error == nil else {
            completion(error)
            return
        }

        if let zone = zone {
            completion(nil)
        } else {
            container.privateCloudDatabase.save(sharedListZone, completionHandler: { (_, error) in
                if let error = error {
                    completion(error)
                } else {
                    completion(nil)
                }
                return
            })
        }

    })
}

 

The Shared Zone’s ID will be static, but randomized, in the sharee’s sharedDB.

If you don’t store the Zone ID from the share metadata, you need to look up any zones in the sharedCloudDatabase using fetchAllRecordZones. There’s no way to get access to them by using just the Zone’s name otherwise.

You will also have a different zone for every different person sharing with you. So if User C and User B are sharing lists with User A, in User A’s sharedCloudDatabase there will be two different zones which you will need to enumerate to get the full collection of shared records.

Once you’ve fetched the record zones and associated them with your data item, you can safely cache it (I use UserDefaults). It’s static, however, you should periodically refresh the sharedDB record zones and these association in the case of a zone being added or removed (for example, an owner can disable sharing or a sharee can accept a share on another device).

One other little trick is to subscribe to the entire shared database using CKDatabaseSubscription and CKModifySubscriptionsOperation. When using CKFetchDatabaseChangesOperation to retrieve the next set of changes, recordZoneWithIDChangedBlock and recordZoneWithIDWasDeletedBlock will let you know zones were added or removed at some point.

UICloudSharingController is just kinda smart

This is the class which you will use to allow your users to share a CKRecord or edit the properties of an existing one. Depending on the properties of an existing CKShare and who is accessing it, the view will adjust accordingly. For example, the owner has the ability to Add People and Stop Sharing when you only allow private access, while the contributors can only view the other party members.

CloudKit Share Screen
The initial CloudKit Share screen
UICloudSharingController’s owner sharing screen.

But also pretty annoying (to use):

UICloudSharingController is not smart enough to fetch an existing CKShare for you – you will need to do that yourself (see section below this). Once you have that, you can use pass it.

Using Apple docs and various other resources, I’ve put together a pretty basic structure to launch this controller and respond to its changes:

@objc
func prepareToShare(button: UIBarButtonItem) {
    let rootRecord: CKRecord = item.record // Either created or cached
    let share: CKShare? = self.share // Cached somewhere or fetch from sharedDB

    let sharingController: UICloudSharingController
    if let share = share {
        sharingController = UICloudSharingController(share: share, container: container)
    } else {
        sharingController = UICloudSharingController { [weak self] (controller, completion: @escaping (CKShare?, CKContainer?, Error?) -> Void) in
            guard let `self` = self else {
                return
            }
            self.share(rootRecord: rootRecord, completion: completion)
        }
    }

    if let popover = sharingController.popoverPresentationController {
        popover.barButtonItem = button
    }
    sharingController.delegate = self.viewModel // Set our delegate... see below
    self.present(sharingController, animated: true) {}
}

// Logic to actually save our share if deteremined by UICloudSharing Controller
func share(rootRecord: CKRecord, completion: @escaping (CKShare?, CKContainer?, Error?) -> Void) {
    let shareRecord = CKShare(rootRecord: rootRecord)
    let recordsToSave = [rootRecord, shareRecord];
    let container = CKContainer.default()

    // We create an operation to save our records
    let operation = CKModifyRecordsOperation(recordsToSave: recordsToSave, recordIDsToDelete: [])
    operation.savePolicy = .changedKeys
    operation.perRecordCompletionBlock = { (record, error) in
        if let error = error {
            print("CloudKit error: \(error.localizedDescription)")
        }
    }

    // And then call our completion handler
    operation.modifyRecordsCompletionBlock = { (savedRecords, deletedRecordIDs, error) in
        if let error = error {
            completion(nil, nil, error)
        } else {
            completion(shareRecord, container, nil)
        }
    }

    container.privateCloudDatabase.add(operation)
}

Using a UIBarButtonItem as a trigger, we can launch the cloud sharing sheet.

Like I mentioned above, a prerequisite to use UICloudSharingController is to actually fetch and cache the CKShare at some point, which you can do by using SKDatabase.fetch(withRecordID:completionHandler:) and using CKRecord‘s share property (which is actually a CKReference and why you need to use fetchWithRecordID to fetch the share).

if let share = record?.share {
    container.privateCloudDatabase.fetch(withRecordID: share.recordID) { (record, error) in
        if let error = error {
            print("CloudKit Error: \(error)")
            completion(nil)
            return
        }

        guard let sharedRecord: CKShare = record as? CKShare else {
            completion(nil)
            return
        }

        completion(sharedRecord)
    }
} else {
    completion(nil)
}

Note: You will also need to check the sharedCloudDatabase in case the user is the sharee to allow them the ability to leave a share.

If you don’t plan on starting a subscription, you should fetch and update your cached the CKShare at some interval relevant to the user interaction because it can change or be removed remotely.

 

UICloudSharingControllerDelegate helps to track if items are being shared

cloudSharingControllerDidSaveShare will be called on the UICloudSharingController‘s delegate whenever the share record is saved (either created or modified). You can use this callback to store a local marker determining if your object has been shared or not, which can come in handy for UI responsiveness (and other functionalities). I typically store this on the entity the CKRecord is actually referencing (often a NSManagedObject in Core Data).

cloudSharingControllerDidStopSharing will only be called for the owner if they stop sharing, either by removing all of the users or tapping the “Stop Sharing” button. You can use this to revert the mark you placed in the didSave equivalent. For the sharee, it will only be called if the user taps “Remove me” from the sharing controller themself (that’s why it’s important to regularly give the sharedDB a skim, since you can be removed from a shared zone or CKShare without being aware).

 

 

And there you have it! A few tips for sharing with CloudKit. Overall it allows some really awesome possibilities and I’m happy that I was able to get list sharing (literally Caramel’s most requested feature) working using it. While there are a few rough edges and I’d love to see the new Core Data CloudKit engine integrated into it better, I think it’s a great and solid way to allow collaboration between users. I’m so excited, I’m trying to think of new apps to really take advantage of CloudKit!

Be sure to look at the complete documentation to see what’s possible. CloudKit backs many of Apple’s services, from News to Notes and it’s really a great cool for building an infrastructure for your application’s store data.

I am Dan Griffin and you can find me on Mastodon

The Blog

Base & Elevated System (and Grouped!) Background Colors

In iOS 13, Apple introduced a slew of new colors that are also dynamic – meaning they will adjust between light and dark modes (and other scenarios, such as high contrast). Of the new colors, the various background colors are pretty pecular: iOS defines two sets of background colors—system and grouped—each of which contains primary,…

iOS iOS 13

Always Taking Inquiries

At the moment I am not taking on many new projects, but am still available for inquiry or questions.

Reach Out To Dan