Update: If you'd like to listen to a conversation about CloudKit, check out iPhreaks episode #226

Synchronizing data with CloudKit

Apple introduced CloudKit in 2014. Since then, it has received many improvements, like the ability to use it outside Apple's platforms and to use it on apps distributed outside the App Store on the Mac.

I believe CloudKit is still not being used much by developers, perhaps because of ignorance or fear. I am writing this article to try and help change that.

Can I use CloudKit?

Unfortunately, whenever talking about Apple and the cloud, the question "can I use this?" arrives. It's true that Apple doesn't have a good history when it comes to cloud services, but the good news is this doesn't seem to apply to CloudKit.

You don't have to take my word for it. Do you use Apple Notes? Photos? iCloud Drive? Activity sharing on Apple Watch? These are all powered by CloudKit and they've been working just fine for me. If they don't work for you, well, then I won’t be able to convince you that CloudKit is good. 😅

Should I use CloudKit?

Even if you're convinced CloudKit is good and works well, it doesn't mean it's the best solution for your particular problem, since there are some applications where CloudKit is the best solution and some where it's not.

Where to use CloudKit

These are the situations for which CloudKit is the most indicated:

Sync private user data between devices

This is perhaps the most obvious use for CloudKit: syncing your users' data across their devices.

Example: a note taking app where the user can create and read notes on any device associated with their iCloud account.

Alternatives: Realm Mobile Platform, Firebase, iCloud KVS, iCloud Documents, custom web app.

Store application-specific data

By using the public database on CloudKit, you can store data that's specific to your app (or a set of apps you own). Let's say for instance you have an e-commerce app and you want to change the colors of the app during Black Friday. You could store color codes in CloudKit's public database and load them every time the app is launched.

Alternatives: Realm Mobile Platform, Firebase, custom web app.

Sync user data between multiple apps from the same developer

If you have a suite of apps, they can all share the same CloudKit container so users can have access to their data for all of your apps on every device they have associated with their iCloud account.

Alternatives: Realm Mobile Platform, custom web app.

Use iCloud as an authentication mechanism

You can use CloudKit just to get the user's unique identifier and use it as an authentication method for another service.

Send notifications

You can use CloudKit to send notifications, eliminating the need to use a 3rd party service or a VPS.

Alternatives: Firebase, custom web app.

Where NOT to use CloudKit

Now we've seen some applications for which CloudKit is best suited for, let's see some examples where it's not.

Store and sync documents

If your app works primarily with documents, CloudKit is probably not the best tool for the job. In this case you'd be better of using Google Drive, DropBox, iCloud Drive or other similar services. You can store large files in CloudKit, but it might not be the best solution if you have a document-based app.

Examples: text editors like Pages, image editors like Pixelmator and Sketch.

Sync user preferences

To store simple user preferences or very small amounts of data, use iCloud KVS (NSUbiquitousKeyValueStore).

Example: your app has a simple boolean preference to show or hide a toolbar and you want this preference to sync across a user's devices. You can do this with CloudKit, but it'd be overkill.

Why not just use an alternate service like Firebase?

CloudKit is a 1st party technology which comes pre-installed on all devices, doesn't require a separate authentication besides the user's iCloud account, has powerful features (like you'll see in the rest of this article) and has a great chance of continuing to exist and be supported for the foreseeable future. These are the main reasons why I think CloudKit is a better option than most 3rd party services.


How much does it cost?

This is a very common question when talking about CloudKit and it is frequently misinterpreted by developers.

Like Craig said when introducing CloudKit:

CloudKit is free… with limits
CloudKit is essentially free….. with limits 🙊

But what does that mean?

Simply put: you're not going to be paying for CloudKit. Period.

What Apple has done is they have created a system which prevents abuse. Therefore, if you do a "regular" use of the service, it'll always be free.

From WWDC 2014, session 231:

We don’t want to prevent legitimate use.
We just don’t want anyone abusing CloudKit.

But wait, there's more! The private database consumes your user's iCloud quota, so if you're only using the private database on your app, you'll never even start to consume your app's quota.

Even if you're using the public database (which counts against your app's quota), you have very generous limits and they scale with the number of users of your app.

Simulation of an app's public database quota for 10 million users

CloudKit in practice

With the introductory stuff out of the way, let's start coding. I'll be explaining various concepts about CloudKit with small code snippets showing how to use them in practice.

Enabling CloudKit on your project

Before we can use CloudKit, we have to enable it for our app in Xcode's capabilities panel.

Enabling CloudKit in Xcode

When we do that, Xcode talks to Apple's servers to update our app's provisioning profile and create our app's default container.

Notice that when we enabled CloudKit, Xcode automatically enabled push notifications, that's because CloudKit's subscriptions are powered by the Apple Push Notification Service (APNS — more on this later).

Container

A container is nothing more than a little box where you put all of your users' data. The most common configuration is to have a single container per app, but you can have an app that uses multiple containers and you can also use the same container across multiple apps.

Containers are represented by instances of CKContainer.

Accessing the default container

To access your app's default container, you use the default method in CKContainer.

Accessing your app's default container

Creating and accessing a custom container

If you want your users to be able to access their data on more than one of your apps, perhaps an iOS and a macOS version of the same app, you must create a custom container that will be used by both apps.

The name of the container must use the reverse DNS notation. For this example I've created a container named iCloud.br.com.guilhermerambo.KitchenContainer.

Creating a custom container that can be used across multiple apps from the same developer

To access this custom container, you just initialize a CKContainer with its identifier.

Accessing the custom container

Remember that if you're using the default container, you can always just use the default method of CKContainer. I'll be using the default container in the rest of the snippets for brevity.

Database

A database is where you're going to be storing your users' data and they are represented by objects of the type CKDatabase.

Every CloudKit container has three databases:

Private Database

This is the database where you'll be storing your user's private data. Only the user can access this data through a device authenticated with their iCloud account. You as the developer can't access data in your users' private databases (although you can access your own private database for debugging).

To access the private database, you use the privateCloudDatabase instance method from CKContainer.

Public database

This is the database where you'll be storing global app data relevant to all users of the app, this data can be created by you using the CloudKit dashboard or a custom CMS or it can be data generated by your users that should be visible to other users.

Although the database is public, it's possible to restrict access to its records by using security roles, but I won't be talking about them in this article.

To access the public database, you use the publicCloudDatabase instance method from CKContainer.

Shared database

With the introduction of iOS 10 and macOS Sierra, Apple added sharing to CloudKit. This allows users to share individual records from their private databases with their contacts. The shared database is used to store these records, but we never interact with it directly.

Zone

A zone is like a directory where you store your records. All databases on CloudKit have a Default Zone. You can use the default zone to store your records but you can also create custom zones. Only the private database can have custom zones — they are not supported in the public database.

Some of CloudKit's features like saving related records in batches and sharing can only be used with custom zones. Zones are represented by objects of the type CKZone.

Record

Records are objects of the type CKRecord and are the main object you use to talk to CloudKit. A CKRecord is basically a dictionary where the keys become fields on the database's tables.

Keep in mind that even though CloudKit's databases are schemaless (you don't have to define your schema by hand), this is only true in the development environment. After you deploy your app, you can only change your schema by changing it on the development environment first and then publishing the changes to the production environment.

Supported data types

Although CKRecord is basically a dictionary, this doesn't mean you can store any type of data in CloudKit. These are the types you can use as values in CKRecord:

String: Apple recommends String for small amounts of text
NSNumber: Swift's numeric types are automatically bridged for you
Data: you could use Data to store custom objects serialized with NSCoding
Date: dates and times can be stored in CloudKit directly
CLLocation: very useful for location-based apps (more on this later)
CKAsset: used to store big files (photos and videos, for instance)
CKReference: a reference that points to another record

Besides all of the types above, any key in CKRecord can contain an array of any of the supported types, provided that it only contains elements of the same type.

Creating a Record

Lets say we're creating an app where users can register movies. We'd probably have a "Movie" record. In this case, "Movie" will be our recordType.

To create a movie record, we initialize a CKRecord:

Initializing a movie record

With that object created, all we have to do is to set its properties. Inside the view controller where the user enters the movie's data, we would probably update our record when a text field text's is changed:

Updating a movie record

Now you might be asking: "what the heck is a CKRecordValue? 🤔".

CKRecordValue is a Swift protocol adopted by objects which CloudKit supports. The issue here is that when using Swift, the compiler doesn't seem to know that String conforms to CKRecordValue, so we have to do that ugly typecast.

Improving our code with a custom subscript

To improve this, when working with CloudKit I always create enums for the fields of my records and add an extension to CKRecord with a custom subscript which takes that enum. I know it sounds complicated, but it's actually really simple.

First, let's create that enum:

Enum representing the keys in a movie record

Now we can add a extension to CKRecord:

Extension to CKRecord so we can set keys in movie records using our custom enum. Pretty swifty 😎

Now, to change the values in our records, our code will look a lot cleaner:

Using our custom enum to set the values of a movie record

Remember that with this custom subscript we're still limited to the data types supported by CloudKit. If we try to set a key to an unsupported type, the field will be nil.

Another important detail: CKRecord is NOT a value type, so when you pass objects of the type CKRecord, you're passing them by reference. This means that if you have a property that is a CKRecord and you add a didSet observer to it, it will not be executed when one of its values is changed.

In "real life" you should probably be using your own models (preferably value types like structs) and converting them to/from CKRecord when dealing with CloudKit.

The entire code for this section can be found in SimpleRecordTableViewController.swift.

The CloudKit Dashboard

Now that we know how to create records on CloudKit, it'd be cool to have some means of knowing what's going on at the server when we save our records.

Apple created a tool for this, the CloudKit Dashboard. In the dashboard we have access to all of our containers, databases, record types and more.

First of all, using the sample app, let's create a movie record:

Creating a movie record using the sample app

With our record created, let's go to the dashboard. The first step is to open the menu at the top-left and choose which container we want to see.

Selecting the container

With the container selected, we see a list of record types. Every database on CloudKit comes with a record of type User by default. This record type is used to store the user records (more about them later).

Our movie record

To see the record stored on the public database, we selected the option "Default Zone" in "Public Data".

Default Zone selected

Now the dashboard is telling us that we need an index for the record ID. Just click "Add Record ID Query Index" and then the dashboard is going to show the record we created using the app.

The record we created using the sample app

Note that, apart from the data we have explicitly added to the record, CloudKit adds some metadata automatically:

Record Name : this is the unique identifier for the record, used to locate records on the database. We can create our own ID or leave it to CloudKit to generate a random UUID.

Created : the date/time of creation. This can be accessed using the creationDate property of CKRecord

Created By : the ID of the user who created the record. Can be accessed using the creatorUserID property of CKRecord

Modified : the date/time of modification. This can be accessed using the modificationDate property of CKRecord

Modified By : the ID of the user that made the last modification to the record. Can be accessed using the lastModifiedUserRecordID property of CKRecord

User Records

In the last section you learned that CloudKit automatically creates a record of type User for your databases. This record contains by default only the unique identifier for the user. This user record identifier is unique per container, which means that a single user will have the same identifier between zones and databases on the same container, but if you happen to use multiple containers on your app, the same user will have different identifiers for each one.

We can do quite a lot with this user record:

  • Know whether the user is logged in to iCloud
  • Get the user record from the container
  • Get the user's full name
  • Get the identifiers for the user's contacts who have corresponding records on the same container
  • Update it with data that's useful for our app
  • Be notified of changes in the user's iCloud account's status

All of the examples in this section are available in UserViewController.swift.

Knowing whether the user is logged in to iCloud

There are many situations where we might have to know if the user is logged in to iCloud on the current device to decide whether a certain feature of the app should be enabled or even prevent the user from doing anything if not logged in.

Notice: if you decide to prevent the user from using your app when an iCloud account is not available, make sure to include a very detailed explanation on your app's review notes for Apple, if you can't explain why your app needs authentication to work, your app may be rejected.

To get the status of the user's iCloud account, we use the accountStatus method from CKContainer

Determining the status of the user's iCloud account

Fetching the user record

To fetch the user record, we have to get its ID first. We use the fetchUserRecordID method from CKContainer to do this.

Fetching the user record identifier

Now that we have the record ID for the user record, we can use the fetch method from CKDatabase to get the actual user record.

Fetching the user record

In the example above, I'm using publicCloudDatabase , but I could be using privateCloudDatabase . Which database you use will depend on your application, since I'm only using the public database in the sample app, I decided to use it here.

You can actually have two separate records for the same user: one on the public and another on the private database. Even though both will have the same identifier, the data they contain can be different. You can use the public record to store information such as an avatar and nickname and the private record to store e-mail, address and other sensitive data.

Getting the user's full name

To get a user's full name from iCloud, we need to ask for permission by using the requestApplicationPermission method from CKContainer , with the option .userDiscoverability . There will be an alert asking the user for permission.

After getting the user's permission, we use the discoverUserIdentity method from CKContainer to get the user's identity. This identity contains the user's full name as a PersonNameComponents value, which we can format using a PersonNameComponentsFormatter

Getting the user's full name using CloudKit

Discovering user contacts who use the app

To get a list of records for the user's friends using the same app, we can use the discoverAllIdentities method from CKContainer .

Getting a list of contacts that have a user record on the same container

Adding extra information to the user record

For our sample app, let's say we want to list the movie records with the name and avatar of the user who registered them. Unfortunately, Apple doesn't offer a way for us to get the user's iCloud avatar, but we can offer this feature by adding a custom field to the user record.

To accomplish this, we'll have to learn about a new class: CKAsset

CKAsset is an object used to store large files on CloudKit. Apple recommends that any field that's larger than a few kb be stored using CKAsset

Working with CKAsset is really simple: you initialize it with an URL to a local file you want to upload to CloudKit and add the CKAsset as a value to one of a record's keys.

When that record gets saved, CloudKit will take care of everything. In the sample app I'm using it to store images that will be used as avatars and there's absolutely no validation or processing of the image, but for a real app you'd absolutely have to check the type of file and probably scale it down so if the user selects a huge image file it doesn't consume too much bandwidth.

I added a button to the interface which opens a UIImagePickerController so the user can select a picture from the library to use as an avatar. The snippet bellow shows what happens after the user selects an image:

Uploading the user's avatar to CloudKit

In the snippet above, imageURL is a URL to a local file, if you try to initialize a CKAsset with a remote URL, there will be an exception and your app will crash.

The save method is used both to create and update records. When you pass it an existing record, CloudKit will update the keys that have been changed since the record was last saved.

As you can see, it's easy to add a new custom field to the user record and upload files to CloudKit.

Updating the user's avatar

Observing changes to the iCloud account's status

Something that can happen while your app is running is the user can open up iCloud preferences and change the logged in iCloud account, or just log out.

If your app changes depending on the logged in user, you must update its state to reflect these changes.

To be notified of changes to the iCloud account's status, all you have to do is register an observer with the .CKAccountChanged notification name.

Registering an observer for iCloud account status notifications

In the sample app, I'm calling the method that's responsible for doing all of the user discovery stuff, so whenever the iCloud account changes, the interface will update accordingly.

Changing the iCloud account while the app is running

Queries

Now that we know how to store data on CloudKit, let's learn how we can retrieve this data.

The most simple way to fetch a record from CloudKit is by using a record ID, like we've seen before when we fetched the user record.

If we want to perform more advanced queries, filtered based on other keys, we'll have to use the CKQuery class. With CKQuery we can specify a predicate to filter our search.

If you're not familiar with NSPredicate , I recommend that you take a look at the documentation. It's a very powerful class that's used a lot with Apple's APIs.

To run a query on CloudKit, we'll be using the CKQueryOperation class. Performing queries is one of many things in CloudKit that uses operations.

Fetching all records of a specific type

This is the most simple one. Let's run a query to get all movie records from our database. The first step is to construct a query with the record type and a predicate, since we want all records, we can just use a predicate with a value of true .

Using a predicate with a value of true to configure a CKQuery and a CKQueryOperation

Now that we have an operation, before executing it, we have to set the closures that will be called when new data is available or when there's an error with the operation.

Setting the operation's callbacks

CKQueryOperation has two callbacks: one is called for every record that gets fetched from the cloud and the other one is called at the end of the operation, after all records have been downloaded (or if an error occurs).

There are two parameters in the completion callback that deserve mention: cursor is an object of the type CKCursor that can be present at the end of the operation in case there are more results to be downloaded. If your query returns a very large amount of records, you'll have to run multiple operations to fetch all of them, passing the cursor from the last operation to each subsequent operation.

The error parameter is also very important — through it you'll know whether an error occurred and what's the nature of the error. Some errors on CloudKit are recoverable, which means you shouldn't just throw an alert to the user the first time an error occurs. Depending on the error, CloudKit will even tell you how many seconds to wait before retrying the operation (and you should definitely follow that recommendation).

Finally, after configuring our operation, we execute it by adding it to the database.

Executing an operation

Performing a textual search

Another very common type of query is the textual query. Users may want to search movies by title. Fortunately, CloudKit deals very well with this and we can construct a simple predicate to take care of it.

Query to search movies by textual information (like title)

The predicate self contains %@ means "look for this value in every key that contains text".

Performing a search based on geographical coordinates

We can even do queries based on location using CloudKit. In my example I've used the device's current location to search for movies shot at locations within a 500km radius.

The predicate for this location based search looks like this:

Location based search

In the snippet above, location refers to the key in our record, currentLocation is a CLLocation value with the user's current location and radius is a Float with the radius (in km) to be used when doing the search.

Here's a gif showing these three types of queries in action:

Three types of queries

You'll find the code for this section in QueryTableViewController.swift

Performing queries on the database gives us complete flexibility to get relevant information for our app, but CloudKit has something even cooler than this: we can create persistent queries that run every time a record on the database is updated and notify our app via push notifications. These persistent queries are called subscriptions.

Subscriptions

Remember I talked about sending notifications using CloudKit? That's what subscriptions allow us to do.

Through subscriptions, we can register to be notified every time some change happens on the database. Therefore, when a new record is inserted, CloudKit will send us a push notification. These notifications can be just content-available notifications (silent), or regular notifications that show alerts and/or badge the app's icon.

Using silent notifications, we can keep our app up-to-date with the latest data every time a change is made to the database, because this type of notification gives our app the opportunity to perform a background fetch.

Creating a subscription

To create a subscription that sends notifications, we first need to get the user's permission to send notifications and tell the system we want to get remote notifications.

Getting permission and registering for remote notifications

If you want to use only the silent notifications (content-available ), you don't have to ask permission, you can just call registerForRemoteNotifications . In the sample app I'm using notifications with alerts and sounds, so I needed to ask for permission through UNNotificationCenter .

Now that we have the user's permission, we can create a subscription with CloudKit. The subscription is an object of type CKSubscription .

Creating a subscription

recordType is the type of record we want to get notified about.

predicate defines the query to be executed to determine whether a notification will be fired. Like I mentioned earlier, subscriptions are like persistent queries that run on the server after each update on the database, it's through this parameter that we determine which query this will be.

Remember the location-based query we constructed earlier? We could register a subscription using that same predicate, making the user get a notification every time a movie shot near his location is added.

options is a list defining in which circumstances the notification will be fired. We can get notified when a record is created, an existing record gets updated or deleted, or all three at the same time.

Configuring the notification itself

With the subscription created, we now have to define how the notification for this subscription will look like. To accomplish this, we use CKNotificationInfo :

Configuring the notification for a subscription

alertLocalizationKey is a key in the app's Localizable.strings file to be used as the format for the alert. This parameter is necessary when you want to include data from the record in the alert. In the example, I'm including the title of the movie:

“movie_registered_alert” = “%@ has been registered, check it out!”;

alertLocalizationArgs contains the keys from the record that should be used to populate the placeholders in the text. I'm using the title key.

desiredKeys are the keys from the record that should be sent with the notification.

Saving the subscription

Now that we have created and configured the subscription, we just have to save it like any other record.

Saving the subscription

Remember that the subscription must be saved on the database for which you want to be notified.

With the configuration above, when creating a new record on another device, my Apple Watch and iPhone got the following notification:

Notification on the Apple Watch
Notification on the iPhone

The code for this section is in SubscriptionViewController.swift.

Architecting for sync

What we've seen until now has been an introduction to the basic concepts of CloudKit. To actually create an app that syncs user data between devices efficiently and correctly, there's a lot more to be done.

To try and help you develop this on your app, I've created a simple note taking app (like Apple Notes) which uses an offline-first architecture for synchronization.

The basic architecture looks like this:

Notes are saved locally to a Realm database. Through Realm Notifications, the sync engine is notified when a note is added, updated or deleted from the local database. These changes are recognized by the sync engine, which converts the local models to CKRecord objects and sends them to CloudKit.

The same process happens in reverse when the change occurs on CloudKit: the app gets a remote notification, gets the information about which records have been added/changed/removed and replicates those changes to the local database. Changing the local database triggers an UI update.

Something you must be aware of when doing this is sync loops. If a change on Realm causes a change on CloudKit and a change on CloudKit causes a change on Realm, we have an infinite loop. To avoid this problem, the sync engine registers a notification token with Realm so changes made by itself don't trigger an upload to CloudKit.

Error handling

It's very important to keep an eye out for errors when dealing with CloudKit. Many developers just print errors or show alerts for the user when an error occurs, but that's not always the best solution.

The first thing you have to check is whether the error is recoverable. There are two very common cases that can cause a recoverable error to occur on CloudKit.

Temporary error / timeout / bad internet / rate limit

Sometimes there can be a little glitch with the connection or Apple's servers that can cause temporary errors. Your app may also be calling CloudKit too frequently, in which case that server will refuse some requests to avoid excessive load. In those cases, the error returned from CloudKit will be of the type CKError , which contains a property retryAfterSeconds . If this property is not nil, use the value it contains as a delay to try the failed operation again.

On my projects, I always have a helper function that looks like this:

Helper method to retry a failed CloudKit operation following the recommended time delay

This method helps dealing with recoverable CloudKit errors. It takes an error returned from a CloudKit operation, a block to be executed if the operation can be retried and it returns the error in case the operation can not be retried.

Conflict resolution

Another common error is a conflict between two database changes. The user may have modified a register on a device while offline and then made another, conflicting change, using another device. In this case, trying to save the record may result in an error of the type serverRecordChanged .

The userInfo property for this error will contain the original record before the modifications, the current record on the server and the current record on the client. It's up to your app to decide what to do with this information to resolve the conflict. Some apps show a panel for the user to choose which record to keep, some merge the content of the two records automatically, some just keep the most recently modified record.

The entire code for this section can be found on my GitHub.

Conclusion

That's it! I hope this article was useful for you to get a better understanding of CloudKit and I hope this inspired you with some ideas to use it for your next project.

Further reading