NOTE: There's a new and improved version of this article on my blog.

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.

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.

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.

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.

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.

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.

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.

Sync user preferences

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

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.

CloudKit is essentially free….. with limits 🙊
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

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.

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.

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

Database

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

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).

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.

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.

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.

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:

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.

Initializing a movie record
Updating a movie record

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.

Enum representing the keys in a movie record
Extension to CKRecord so we can set keys in movie records using our custom enum. Pretty swifty 😎
Using our custom enum to set the values of a movie record

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.

Creating a movie record using the sample app
Selecting the container
Our movie record
Default Zone selected
The record we created using the sample app

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.

  • 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

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.

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
Fetching the user record

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.

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.

Uploading the user's avatar 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.

Registering an observer for iCloud account status notifications
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.

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
Setting the operation's callbacks
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)

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.

Location based search
Three types of queries

Subscriptions

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

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
Creating a subscription

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

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
Notification on the Apple Watch
Notification on the iPhone

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.

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.

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.

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

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 .

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

I make software for iOS and Mac, write at 9to5mac.com and talk at stacktracepodcast.fm

I make software for iOS and Mac, write at 9to5mac.com and talk at stacktracepodcast.fm