CloudKit: The fastest route to implementing the auto-synchronizing app you've been working on?

Apps often need to store their data model objects in the cloud. This may be driven
by the need to share with others, or simply to keep your apps in sync across
multiple devices.

For many app developers, the work required to spin up a web service is too much.
You have to find a place to host the app, write all the server-side code, and only
then can you begin your client-side implementation. There are numerous ways to
accomplish this, but they all require work. And so much of the work would be the
same from project to project.

Apple has released CloudKit to address this need. With CloudKit, Apple has put together
a NoSQL-style storage system. The operating system worries about keeping the iCloud
containers in sync, and it does this in the background. Notification of changes
comes via push notifications—including the ability to background wake your app.

To play with CloudKit, I decided to see how simple it would be to throw together a
chat application. This would function similar to IRC or the old AOL chat rooms.
We won’t spend any time developing the security functions you’d want in an app like
this, but it will show you how to simply create objects in the public database.

Introducing Fatchat

My Fatchat app allows you to post messages in channels. When you are looking at a channel
and a new message comes in, it will be added to the window automatically. If you
are not looking at the app and a new message comes in, you will get a push
notification. When you leave a channel, you will stop getting notifications for it.
(Really, a more elegant subscription mechanism should be employed, but I shall
leave that as an exercise for you, dear reader.)

The Plan

In a given CloudKit record zone, you have record types. These are analogous to
tables in a relational database. CloudKit stores timestamp and user information each
time an object is saved. So when we create our channel, we’re just going to give
it a name.

A channel will contain many message objects, and several subscription objects as
well. You will subscribe to a channel by entering it, and unsubscribe by leaving.
Therefore, we can query those subscriptions to see who is in the channel.

The Setup

Create a new project!

That bundle ID must be globally unique for CloudKit’s use. Once you’ve created the
project, click on the project’s “Capabilities” tab. Enable iCloud and
then check CloudKit.

Down below where Steps are listed, you may have some errors. You should also have a
“Fix Issues” button. Clicking that button should make the errors disappear if you are
the admin of your development team.

Leaving aside the development of the UI, let’s jump right into the bits of code that made CloudKit work.

Databases and Zones

Each CloudKit container has one public database, and a private database for each
iCloud account using the app. Getting a handle to either one is as simple as
calling -publicCloudDatabase or -privateCloudDatabase on the default CKContainer.

In the private database, you can put your data in a Record Zone. Record zones
are an additional layer of organization, analogous to a database schema. The
record zone also offers one important feature: atomic commits of groups of
records, wherein if a single record fails to save, they can all be rolled back.

Public databases do not have the concept of zones. If you query the zones, you will
get back just the _default zone. You can pass in this value as needed, or if you’re
using the _default zone, it is permissible to pass in nil.

Creating a Record

Saving records couldn’t be much simpler. After instantiating a CKRecord object,
you treat it like a dictionary. Once you have added all your values to the object,
you ask the database to save it. There are a couple ways of accomplishing this.
You can create a CKModifyRecordsOperation, which gives you per-record and per-batch
control over the process, or you can call a convenience method on the database.
The convenience method is much simpler, so we’ll use that.

The error received should really be handled. In cases where the error is a concurrent
write error, you will receive three records with the error: The failed record, the
conflicting record and the common ancestor of both.

Cascading Deletes

When we create messages or subscriptions, we’ll want to give them a reference
back to the channel. This is a form of foreign key relationship. Because the channel
is the parent and the message is the child, the reference is part of the message.
And if you delete the channel, we should also delete all the messages.

Attaching Files

Any large chunk of BLOB data is considered an “asset” in CloudKit parlance. We give CloudKit a reference to the file. It will copy the file into the CloudKit container. After that copy completes, the fileURL property of the CKAsset object will be different. You should never store that value, but rather, access it when needed.

// Attach an asset if given one.
if(assetFileUrl){CKAsset*asset=[[CKAssetalloc]initWithFileURL:assetFileUrl];[recordsetObject:@(assetType)forKey:AssetTypeKey];[recordsetObject:assetforKey:AssetKey];}

Finding Your Records

Querying is built on the venerable NSPredicate class. In this example, I ask for
all records. You could add something like name = "Secret Channel" to get as
specific as needed, but for our example, we want anything that matches.

Getting Notified of Changes in Real Time

CloudKit makes extensive use of the Push Notifications system. In order to
begin receiving these notifications, you will create a CKSubscription
that responds to any changes in the zone. The behavior of the notification is
defined in a CKNotificationInfo object.

We start by creating a model notification. This will tell CloudKit what we want
our notifications to look like.

The CKNotificationInfo object is used as a template for the generated
notifications. Each received notification can be turned into a
CKNotification. The CKNotificationInfo object has the following
properties:

alertBody: This will be the alertBody of the received CKNotification.

alertLocalizationKey will pull the alert body from the Localizable.strings
file. If this value is set, alertBody is ignored.

alertLocalizationArgs contains an array of keys that will map to an array
of values in the CKNotification. By that I mean, an array of
@[ @"firstName", @"lastName" ] would result in a received array of
@[ @"John", @"Doe" ]. In the example above you can see how it is used to
represent sequential values.

alertActionLocalizationKey is used to look up the action text from Localizable.strings. If it is nil, there will be only an OK action
available to dismiss the alert. If a key is provided here, the action text
is provided to prompt the user to launch the app.

alertLaunchImage identifies an image in your bundle to be shown as an alternate
launch image when launching from the notification.

soundName identifies a sound in your bundle to be played with your notification.

shouldBadge tells us whether we should increment the app icon’s badge value.

shouldSendContentAvailable allows the action to launch the app if it’s
not running. Once launched, the notifications will be delivered, and the app will
be given some background time to process them.

Now let’s write code to discover ourselves, find the associated record ID and
attach the first name and last name to it. A problem is that we cannot discover
ourselves until we’ve created a record. (I’m glad this is software, not a life analogy.)

After running it, my info was populated in the Dashboard. To see it, I had to refresh
the entire web page; the schema appears to be cached in the browser.

The Promise of CloudKit

CloudKit was an incredibly fast way for me to set up shared relational data
across many devices. The performance was decent, too; replication of Fatchat
messages typically took only a second or two. Those two things lead me to see
a lot of promise in this API.

But there are thorns on these roses, too. It’s iCloud-only, of course, which
means Apple-only, without a simple migration plan if you branch
out. And the storage tiers may not fit your usage: The website seems to
indicate that it’s free until you surpass a million users, but a
closer look indicates that exceeding the individual limits may also impose
a cost. And those limits aren’t as generous as at first they appear, if you
decide to store rich media in your app. Finally, and most importantly, the privacy model at the moment is “you” or “everybody”—CloudKit doesn’t yet contain a
mechanism for securely sharing an item with someone else.

If these limitations are okay with you (and for several of my apps, they’re
fine), then CloudKit may well be your fastest route to implementing the
auto-synchronizing app you’ve been working on.