CKSharing step by step

A guide to getting CloudKit items shared

Adam Miller
9 min readFeb 3, 2017

CKSharing was introduced at WWDC 2016, it was a major part of two of their developer presentations: CloudKit Best Practices, and What’s new with CloudKit. I normally groan in my head when someone says I need to watch a WWDC video before I embark on a coding project, but this is absolutely true for this one. You really do have to watch both of these videos to have even a basic understanding of what’s going on.

Also, CloudKit Sharing is maybe the least documented major feature of a tentpole library I’ve ever seen released by Apple. I feel almost a civic duty to write this article so that others won’t struggle with the same things that I did. One of the reasons for the lack of documentation is that I don’t think CloudKit gets used very often by developers. Most of my dev friends I talk to are too busy using a JSON parsing custom backend, Firebase, etc… to pay any mind to what the latest and greatest CloudKit offerings are. Their loss!

The project:

I’ve been working on an app for a couple years now (on and off, mostly off) that helps me track employee hours. The concept is pretty simple: There’s a Manager, and they have Employee(s) that report hours to him/her. The employees are allowed to add, edit, delete hours themselves, and the manager will get notifications anytime any of this happens. The app then adds all these hours up so payroll can be figured out. You can imagine that additional features could easily be built off of a structure like this, such as creating paychecks, calculating withholding, etc… We’re just going to focus on the sharing hours part.

CloudKit Structure:

Since a manager could have multiple employees, we’ll need an Employee CKRecord type in our schema. It will contain name, address, pay rate, etc… as values. The hours are going to be represented by a WorkEvent CKRecord that I’ve also created in my schema. Each WorkEvent will have a startTime, endTime, comments, etc… plus a reference back to the Employee. Now we just need a place to put these records.

Public database: this is where you’d store things you want everyone to be able to access. There are no custom zones in Public databases, everything exists in the default zone.

Our data (mentioned above) is obviously private to these two parties, so using the public database is a no-no. It’s possible to do publicly through key-sharing, but it’s basically security through obscurity, which is less than ideal.

Private database: This is where you store records that you don’t want the public to see. The private database has a default zone, and users can create custom zones. Important: put anything you want to share in custom zones in your private database.

For this app, the Manager is going to use his/her private database to store all the records for this app. We’re going to create a custom zone for each employee, which will have one Employee CKRecord and many WorkEvent CKRecords. The manager will subscribe to changes in their private database.

Shared database: This database holds the “windows into objects” that you have been given access to. The records you see in the shared database exist on other people’s private databases. You just have a window into that object. Another way of saying it is that you are a “contributor” to the items in your shared database, not the owner.

For this app, the employee will access their WorkEvents and Employee CKRecords through the shared database. They will subscribe to changes in the shared database. The Manager will not have any zones in their shared database, because that info lives in their private database. This is super important to understand.

Of note: Don’t let the term “shared zone” trip you up. There’s no such thing as a CKRecordZone that you put records in where it’s just automatically shared with another user. The shared zone is just a zone created for you to access and add shared records, but it by itself does not cause the objects to be shared, more on this later.

What this will look like:

We’ve given Alice a window into the manager’s records through the shared database. Nothing at all happens on the Public Database for either user. You’ll notice Alice does not see all of my records.

The CKShare Object

CKShare is a CKRecord that acts like a portal from you private database to another user’s shared database. The CKShare is attached to a CKRecord by the server, all you do is create the CKShare and indicate which record you want to attach it to. You add it just like any other CKRecord, through a CKModifyRecordsOperation. When you add a CKShare object, it will create a record type in your dashboard, which you can inspect. It even tells you through drop downs who the various users are and if they’ve accepted the invitation or not!

But before we add a CKShare record to our database, we need to figure out who we’re going to share this data with.

Let’s find someone to share with

There are two ways to identify who you want to share something with. One is a “Custom” route, and the other way is the UICloudSharingController route.

Custom: I’m not going to go too much into the custom route, but it involves getting permission for userDiscoverability using:

CKContainer.default().requestApplicationPermission(...)

You’ll need to include that you’re requesting .userDiscoverability, and set a completionHandler. This method will get let other users see that you have used this app and allows you to be discovered. This will allows users to find and manipulate CKUserIdentity objects, which let them set up sharing. You can make some pretty easy to use experiences doing this, but its a lot more development work. There’s an easier way…

UICloudSharingController: As you can tell from the “UI” part, this is a view that you present to the user that lets them select who they’d like to share with. In creating this view, you also have to indicate the object you’re going to share, as well as creating the CKShare object. Let’s look at some code.

// Note: employeeRecord is the CKRecord I need to sharelet share = CKShare(rootRecord: employeeRecord)share[CKShareTitleKey] = "Some title" as CKRecordValue?share[CKShareTypeKey] = "Some type" as CKRecordValue?let sharingController = UICloudSharingController
(preparationHandler: {(UICloudSharingController, handler:
@escaping (CKShare?, CKContainer?, Error?) -> Void) in
let modifyOp = CKModifyRecordsOperation(recordsToSave:
[employeeRecord, share], recordIDsToDelete: nil)
modifyOp.modifyRecordsCompletionBlock = { (record, recordID,
error) in
handler(share, CKContainer.default(), error)
}
CKContainer.default().privateCloudDatabase.add(modifyOp)})sharingController.availablePermissions = [.allowReadWrite,
.allowPrivate]
sharingController.delegate = self
self.present(sharingController, animated:true, completion:nil)

First we create a CKShare object using the root record we’d like to share. In our instance, we’re going to share an Employee record (more on why this is in a bit)

Then we set title and type keys. These show up in your dashboard, I wish i could tell you what these do, but I haven’t figured out exactly what their purpose is yet :-)

Then we create our UICloudSharingController, which takes a preparationHandler as an argument. In the preparationHandler you need to add both the share and the Employee record to the private database at the same time, so they’re available to be shared.

Then we set permissions, like if we want anyone to be able to access this Employee, or just people we invite. Then we set the delegate to ourselves, and present the sharingController.

Voila!

The name in the middle of the screen is the title of the object that I’m trying to share. In this case I’m sharing an “Employee” record so I’m using Alice’s name, but if you’re sharing a document it would be the document name, etc… The email address below is your iCloud email address. You set the image in the middle of the screen and the title through delegate methods.

func cloudSharingController(_ controller: UICloudSharingController, failedToSaveShareWithError error: Error) {
// Failed to save, handle the error better than I did :-)
// Also, this method is required!
print(error)
}
func itemThumbnailData(for: UICloudSharingController) -> Data? {
// This sets the image in the middle, nil is the default
document image you see, this method is not required
return nil
}
func itemTitle(for: UICloudSharingController) -> String? {
// Set the title here, this method is required!
// returning nil or failing to implement delegate methods
results in "Untitled"
return "Alice Campbell"
}
// There are additional delegate methods, see the docs.

This whole interface is intended to help you send a link to another user. The link is a URL that when opened, will open the app on the other user’s device and call a method in the App Delegate called userDidAcceptCloudKitShare… You need to implement this method in your app delegate, or else the user will not be able to accept a share.

func application(_ application: UIApplication, 
userDidAcceptCloudKitShareWith cloudKitShareMetadata:
CKShareMetadata) {

let acceptShareOperation: CKAcceptSharesOperation =
CKAcceptSharesOperation(shareMetadatas:
[cloudKitShareMetadata])

acceptShareOperation.qualityOfService = .userInteractive
acceptShareOperation.perShareCompletionBlock = {meta, share,
error in
print("share was accepted")
}
acceptShareOperation.acceptSharesCompletionBlock = {
error in
/// Send your user to where they need to go in your app
}
CKContainer(identifier:
cloudKitShareMetadata.containerIdentifier).add
(acceptShareOperation)
}

TL;DR, we’re accepting the share here, and we have an opportunity to put the user in the correct place in our UI to indicate success.

But, I need to share more than one record…

So, as you can see we have shared exactly ONE record at this point. If you’re like me, you’re probably sweating thinking you’ll have to do this for every record you want to share. Thankfully that’s not the case.

CKRecord now has a property called Parent. We use parent to indicate that a new CKRecord is related to a previously shared record. In this instance, we’ve shared the Employee, and now all of the WorkEvents we create will have that shared Employee CKRecord set as their parent. This will make all the “children” WorkEvents visible in the Shared database of the share participants. They are also bound by the sharing terms of that original record sharing.

This is why earlier I mentioned we’d have one Employee record and several WorkEvent records. The Employee record is the backbone for this operation, I share that employee with someone, and then they have access to all the WorkEvents that have that Employee marked as their parent. You can do this with as many record types as you’d like! (ie. Receipts, TaxProfile, Paycheck, etc…)

So let’s make a child record that we’d like to include with our original share.

// "employeeRecord" is the CKRecord that has already been shared with the other users// "event" is a coreData class I've created that is the equivalent of the WorkEvent CKRecord, it contains what I need to set as values for my new CKRecord// "zone" is the zone I need to save this record to (The one that contains all the records for this employee)let newRecord = CKRecord(recordType: "WorkEvent", recordID: CKRecordID(recordName: event.recordName, zoneID: zone.zoneID))newRecord.setValue(event.hours, forKey: "hours")
newRecord.setValue(event.startTime, forKey: "startTime")
newRecord.setValue(event.endTime, forKey: "endTime")
newRecord.setValue(event.comments, forKey: "comments")
newRecord.setValue(event.type, forKey: "type")
let reference = CKReference(record: employeeRecord, action: CKReferenceAction.deleteSelf)
newRecord.setValue(reference, forKey: "Employee")
newRecord.setParent(employeeRecord)

The is a basic CKRecord constructor for a new WorkEvent. You’ll notice I’m setting values based on user input (in the form of a CoreData class I’ve created). I’m also creating a CKReference to the Employee CKRecord, so I can get deletion rule stuff in CloudKit. Lastly, I set the Parent property, which lets other users that have I’ve shared the Employee with, access this new record I’m creating.

That’s basically it

  • Create your records in your private database in custom zones
  • Share those records using a CKShare record once you know who you’re sharing it with.
  • Access records shared with you from the shared database, but remember this is just a window into someone else’s private database.
  • Share additional records by adding the originally shared record under the Parent property of your new records
  • Subscriptions work from their respective databases, ie. creators subscribe through their private database, and contributors subscribe through the shared database.

If there’s something incorrect here or not clear, reach out to me on Twitter.

--

--

Adam Miller

A geeky dad, creator of interfaces, illustrator, and movie buff