CloudKit 101

Feb 25 2020 2:00 AM

Note: this article is a revision of the article I wrote back in 2017. If you’d like to listen to an informal conversation about CloudKit, check out iPhreaks episode #226.

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.

Even though CloudKit is widely used by Apple in first-party apps and by some developers, I believe it has potential to become even more popular as a backend and sync solution for apps in Apple’s platforms, and that’s why I decided to write this introductory article.

Is CloudKit safe to use?

Unfortunately, whenever talking about Apple and the cloud, the question “is this safe to use in production?” arrises. It's true that Apple has had issues before when it comes to cloud services, but the good news is this doesn't 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.

The thing to be aware of is that, even though CloudKit is a server-side technology, it’s still driven by the client — your app — so the success of the implementation largely depends on how well it’s implemented. Luckily, with the introduction of NSPersistentCloudKitContainer last year, the most common use case — syncing private user data across devices — is mostly taken care of for you.

That said, it can still be beneficial to know the ins and outs of CloudKit if you wish to implement your own sync solution or use it for other purposes. If that’s the case, read on.

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.

The public database on CloudKit is a good place to store remote app configuration in general. In my app ChibiStudio, I use it to configure feature switches so that features can be rolled out or rolled back after a build has been released for customers.

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 your service. These days you can also use Sign in with Apple, but that’s a little bit more involved and less transparent to the user than simply getting access to their anonymous user identifier.

Send notifications

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

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 off 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 first-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.

As said by Craig Federighi when introducing CloudKit back in 2014:

CloudKit is free… with limits

1

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 counts against your user's iCloud quota, so if you're only using the private database in your app, you'll never even start to consume your app's quota.

This does mean that if your user is out of space in their iCloud, you’ll receive an error back from the server when trying to save something to their private database. The best way to handle it is to just let the user know about what’s going on.

But even if you are 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:

2

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 for your target

Before you can use CloudKit, you have to enable it for your app in Xcode's Signing & Capabilities panel.

3

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

You can also click the + button in Containers and create a custom container so that you can share it with your other apps or between your app and its extensions. Containers should be named using the reverse-DNS notation, in my example iCloud.codes.rambo.CloudKitchenSink20.

Notice that when CloudKit is enabled, Xcode automatically enables push notifications. That's because CloudKit's subscriptions are powered by the Apple Push Notification Service.

Environments

CloudKit has two environments: the development environment and the production environment. By default, when building the app in Xcode with the debug configuration, the development environment will be used. When distributing the app through TestFlight or the AppStore, the production environment will be used.

It can be useful sometimes to use the production environment in a debug build for testing purposes. You can achieve that by manually editing your app’s entitlements file and setting the com.apple.developer.icloud-container-environment key to Production.

4

Keep in mind that even though CloudKit's databases are schema-less — 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 in the development environment first and then publishing the changes to the production environment. Deploying a development schema to production is done in the CloudKit Dashboard.

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 static property in CKContainer.

let container = CKContainer.default()

Accessing a custom container

If you’ve created a custom container using the + button in the capabilities tab, you’ll have to specify its identifier when initializing an instance of CKContainer.

let containerIdentifier = "iCloud.codes.rambo.CloudKitchenSink20"
let secondContainer = CKContainer(identifier: containerIdentifier)

I highly recommend you always create a custom container so that it’s easier for you to share your CloudKit databases between your app and extensions, or between your app on multiple platforms — iOS, watchOS and macOS, for example.

Remember that if you're using the default container, you can always just use the default static property on 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. Databases are represented by objects of the type CKDatabase.

You don’t create databases in CloudKit, every container already comes with 3 databases:

Private Database

This is the database where you'll be storing your users’ 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 a user’s private database.

During development, if you use the same Apple ID for your developer account as the one you use on your device, you can access your own private data for debugging, using the CloudKit dashboard. If that’s not possible, you can sign in to iCloud in an iOS Simulator with your developer account for testing.

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.

Even though the database is public, it's possible to restrict access to its records by using security roles, so that only the user who created a certain record can get access to it, or to have all data be read-only but certain accounts that are “admin” users and have permission to insert, update and delete information on the public database.

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

Shared database

Back on 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 you don’t have to interact with it directly.

I won’t be covering sharing in this article, but you can learn more here.

Zone

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

Some of CloudKit's features, such as saving related records in batches and sharing can only be used with custom zones. Because of that, I highly recommend that you have at least one custom zone where you save all your users’ records. Zones are represented by objects of the type CKZone.

Record

Records are objects of the type CKRecord and can be considered the model object of 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. Here are the types you can use as values in CKRecord:

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

Let’s say we're creating an app where the user can register recipes. We'd probably have a “Recipe” model, which can be represented as a CKRecord with the recordType set to Recipe.

To create a Recipe record, initialize a CKRecord:

let record = CKRecord(recordType: "Recipe")

With that object created, all you have to do is set its properties, which can be done with a simple subscript:

record["title"] = "Spaghetti Carbonara"
record["ingredients"] = ["Spaghetti", "Guanciale", "Eggs"]

Improving the code with a custom subscript

Notice the stringly typed subscript in the example above. You can improve it by creating a custom subscript on CKRecord for keys specific to the Recipe record type:

extension Recipe {
    enum RecordKey: String {
        case title
        case subtitle
        case ingredients
        case instructions
        case image
    }
}


extension CKRecord {
    subscript(key: Recipe.RecordKey) -> Any? {
        get {
            return self[key.rawValue]
        }
        set {
            self[key.rawValue] = newValue as? CKRecordValue
        }
    }
}

Now, to change the values in records, the code will look a lot cleaner (and be a lot safer):

record[.title] = "Spaghetti Carbonara"
record[.ingredients] = ["Spaghetti", "Guanciale", "Eggs"]

Remember that with this custom subscript we're still limited to the data types supported by CloudKit. If you 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 around, 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.

This detail is not that important, since when working with CloudKit in your app, you should be converting between your own model objects — which can be structs — and CKRecord. This conversion can be automated with something like my CloudKitCodable or code generation, but you may prefer to do it manually so that you can fully control the encoding and decoding process.

The CloudKit Dashboard

Now that you 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 records are saved.

Apple created a tool for this, the CloudKit Dashboard. In the dashboard it’s possible to browse all of your containers, databases, record types and more.

After you create a record in your own private database, you can go to the dashboard and query for records of that type, allowing you to see all data about the record you’ve just created:

5

If you see an error when trying to query a record type in the dashboard, it probably means you haven’t added an index to the recordName. To do that, you can select “Schema” in the top menu, then in the “Record Types” menu select the “Indexes” option, and add an index so that recordName is QUERYABLE.

Notice that, apart from the data that’s been 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. CloudKit is able to generate an ID automatically for us, by I strongly recommend creating a custom record name with your own ID that matches what you have in your local storage. In the example app I made for this article, that’s what I’m doing.

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

Every database on CloudKit comes with a record of type User by default. This record type is used to store the user records, which represent each user of your app. By default it contains 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 in your app, the same user will have different identifiers for each one.

We can do quite a lot with this user record:

Checking if the user is logged in to iCloud

There are many situations where it can be important 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, use the accountStatus method from CKContainer:

CKContainer.default().accountStatus { status, error in
    if let error = error {
      // some error occurred (probably a failed connection, try again)
    } else {
        switch status {
        case .available:
          // the user is logged in
        case .noAccount:
          // the user is NOT logged in
        case .couldNotDetermine:
          // for some reason, the status could not be determined (try again)
        case .restricted:
          // iCloud settings are restricted by parental controls or a configuration profile
        @unknown default:
          // ...
        }
    }
}

Fetching the user record

To fetch the user record, you need to get its ID first. You can use the fetchUserRecordID method from CKContainer to do this.

CKContainer.default().fetchUserRecordID { recordID, error in
    guard let recordID = recordID, error == nil else {
        // error handling magic
        return
    }
    
    print("Got user record ID \(recordID.recordName).")
}

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

// `recordID` is the record ID returned from CKContainer.fetchUserRecordID
CKContainer.default().publicCloudDatabase.fetch(withRecordID: recordID) { record, error in
    guard let record = record, error == nil else {
        // show off your error handling skills
        return
    }

    print("The user record is: \(record)")
}

In the example above, I'm using publicCloudDatabase, but I could be using privateCloudDatabase. Which database you use will depend on your application.

You can actually have two separate records for the same user: one in the public database and another one in the private database. Both will have the same identifier, but 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, you 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, the discoverUserIdentity method from CKContainer can be called to get the user's identity. This identity will contain the user's full name as a PersonNameComponents value, which can be formatted using a PersonNameComponentsFormatter.

CKContainer.default().requestApplicationPermission(.userDiscoverability) { status, error in
    guard status == .granted, error == nil else {
        // error handling voodoo
        return
    }

    CKContainer.default().discoverUserIdentity(withUserRecordID: recordID) { identity, error in
        guard let components = identity?.nameComponents, error == nil else {
            // more error handling magic
            return
        }

        DispatchQueue.main.async {
            let fullName = PersonNameComponentsFormatter().string(from: components)
            print("The user's full name is \(fullName)")
        }
    }
}

Discovering user contacts who use the app

To get a list of records for the user's friends using the same app, you can use the discoverAllIdentities method from CKContainer. This will return identities for people in the user’s address book who have your app installed and have given it permission to access their identity.

CKContainer().default().discoverAllIdentities { identities, error in
    guard let identities = identities, error == nil else {
        // awesome error handling
        return
    }

    print("User has \(identities.count) contact(s) using the app:")
    print("\(identities)")
}

Adding extra information to the user record

Let's say you’d like to list records with the name and avatar of the user who created them. Unfortunately, Apple doesn't offer a method to get the user's iCloud avatar, but this feature can be implemented by adding a custom field to the user record and letting the user manually upload an image. The same technique can be used to include other information in the user’s record.

If you’d like to add something like an avatar to a user’s record, or any large asset to any other type of record, you have to use CKAsset, which is an object used to store large files on CloudKit. Apple recommends that any field that's larger than a few kilobytes be represented by a 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 or retrieved, CloudKit will take care of uploading or downloading it on your behalf, populating the fileURL property of the asset with the URL to a local file your app can read. For a production app, make sure you check the type of file that’s being uploaded and if it’s an image, scale it down and compress it so if the user selects a huge image file it doesn't consume too much bandwidth and storage space.

Let’s say you have a button on the interface that opens an 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:

private func updateUserRecord(_ userRecord: CKRecord, with avatarURL: URL) {
    userRecord["avatar"] = CKAsset(fileURL: avatarURL)

    CKContainer.default().publicCloudDatabase.save(userRecord) { _, error in
        guard error == nil else {
            // top-notch error handling
            return
        }

        print("Successfully updated user record with new avatar")
    }
}

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. Using that method can be convenient, but as you’ll see later in this article, the recommended way to interact with CloudKit is by using operations.

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

Observing changes to the user’s iCloud account 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. Or maybe the user is initially logged out, then logs in while your app is in the background.

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

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

NotificationCenter.default.addObserver(self, 
                                       selector: #selector(userAccountChanged), 
                                       name: .CKAccountChanged, 
                                       object: nil)

When you detect that the current logged in user has changed, you probably want to remove all private data you have cached locally and fetch the data for the new user.

Queries

Now that you’ve learned the basics of how to store data in CloudKit, it’s time to learn how to retrieve that data. The simplest way to fetch a record from CloudKit is by using a record ID, like you’ve seen before when I showed how to fetch the user record.

More advanced queries which filter based on other keys will require the use of the CKQuery class. With CKQuery, a predicate can be specified to filter records. If you’re not familiar with NSPredicate, I recommend reading the docs. It's a very powerful class that's used a lot with Apple's APIs.

To run a query on CloudKit, you use 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 basic one. To run a query to get all recipe records from the database, the first step is to construct a query with the record type and a predicate. Since all records should be retrieved, a predicate with the value true can be used.

let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "Recipe", predicate: predicate)
let operation = CKQueryOperation(query: query)

The operation accepts two closures: recordFetchedBlock, called multiple times during its execution, when a new record is fetched, and queryCompletionBlock, called after the operation is finished, and which may include an error.

let recipeRecords: [CKRecord] = []

operation.recordFetchedBlock = { record in
    recipeRecords.append(record)
}

operation.queryCompletionBlock = { cursor, error in
    // recipeRecords now contains all records fetched during the lifetime of the operation
    print(recipeRecords)
}

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 returned by 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, you execute it by adding it to the database:

CKContainer.default().publicCloudDatabase.add(operation)

In a real-world scenario, you may prefer to create your own DispatchQueue to add CloudKit operations to, so that you can control and observe operations a little bit better:

private let cloudQueue = DispatchQueue(label: "SyncEngine.Cloud", qos: .userInitiated)

private lazy var cloudOperationQueue: OperationQueue = {
    let q = OperationQueue()

    q.underlyingQueue = cloudQueue
    q.name = "SyncEngine.Cloud"
    q.maxConcurrentOperationCount = 1

    return q
}()

You may also want to set the qualityOfService in the operation to .userInitiated to make sure it executes in a reasonable time.

Performing a textual search

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

let predicate = NSPredicate(format: "self contains %@", title)
let query = CKQuery(recordType: "Recipe", predicate: predicate)

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

Performing a search based on geographical coordinates

CloudKit supports queries based on location. Suppose you have an app that stores points of interest with a location property, you can use a device’s location to search for points of interest within a 500km radius, using a predicate such as the one below:

NSPredicate(format: "distanceToLocation:fromLocation:(location, %@) < %f", currentLocation, radius)

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

Performing queries on the database gives you complete flexibility to get relevant information in your app, but CloudKit has something even nicer than this: it’s possible to create persistent queries that run every time a record on the database is updated and notify the app via push notifications. These persistent queries are called subscriptions.

Subscriptions

Remember I talked about sending notifications using CloudKit? That's what subscriptions enable.

Through subscriptions, you can register to be notified every time some change happens on the database. Therefore, when a new record is inserted, CloudKit will send the app 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, you can keep your app up-to-date with the latest data every time a change is made to the database, because this type of notification gives your app the opportunity to perform a background fetch.

Creating a subscription

To create a subscription that sends notifications, you first need to get the user's permission to send notifications and register the app for remote notifications:

UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { authorized, error in
    guard error == nil, authorized else { 
        // not authorized...
        return
    }
	
    // subscription can be created now \o/
}

UIApplication.shared.registerForRemoteNotifications()

If you want to use only the silent notifications, it’s not necessary to ask for permission, you can just call registerForRemoteNotifications.

You can now create a subscription with CloudKit. The subscription is an object of type CKSubscription:

let subscription = CKQuerySubscription(recordType: "Recipe",
                                        predicate: NSPredicate(value: true),
                                          options: [.firesOnRecordCreation])

In the CKQuerySubscription, the recordType is the type of record you want to be 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 you determine which query this will be.

Remember the location-based query I showed earlier? You could register a subscription using that same predicate, making the user get a notification based on a record being created with its location within a 500km radius of the device’s location.

options is a list defining in which circumstances the notification will be fired. You 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, you have to define what the notification for this subscription will look like. To accomplish this, you set up a CKNotificationInfo:

let info = CKNotificationInfo()
info.alertLocalizationKey = "nearby_poi_alert"
info.alertLocalizationArgs = ["title"]
info.soundName = "default"
info.desiredKeys = ["title"]
subscription.notificationInfo = info

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 this example, I'm including the title of the point of interest, the localization key would look like this:

“nearby_poi_alert” = “%@ has been added, 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 in the example above.

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

Saving the subscription

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

container.publicCloudDatabase.save(subscription) { [weak self] savedSubscription, error in
    guard let savedSubscription = savedSubscription, error == nil else {
        // awesome error handling
        return
    }
	
    // subscription saved successfully
    // (probably want to save the subscriptionID in user defaults or something)
}

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

Custom zones and change tokens

What you’ve seen above are the basics of how CloudKit works and how to save and retrieve data. When you’re writing an app to sync private user data, you probably won’t be using queries or query subscriptions to fetch the data, you probably won’t be using just the save method to store records either.

The basic workflow for syncing private user data is as follows:

That’s the initial setup. After that’s done, you should observe changes to the local data and upload them to CloudKit, and observe remote changes and fetch them when your app gets a silent push notification from CloudKit.

To fetch remote changes, you’ll have to store the last change token you received from the server. The change token is like a pointer in a timeline you can use to tell CloudKit to fetch what’s changed since the last time a particular client has had its local data updated from the server.

Here’s how you can fetch remote changes for a custom zone. The custom zone ID can be defined by you and stored as a constant in code. The change token should be stored by your app, usually in user defaults.

let customZoneID = CKRecordZone.ID(zoneName: "KitchenZone", ownerName: CKCurrentUserDefaultName) // CKCurrentUserDefaultName is a "magic" constant provided by CloudKit which represents the currently logged in user

let operation = CKFetchRecordZoneChangesOperation()

let token: CKServerChangeToken? = privateChangeToken

let config = CKFetchRecordZoneChangesOperation.ZoneConfiguration(
    previousServerChangeToken: token,
    resultsLimit: nil,
    desiredKeys: nil
)

operation.configurationsByRecordZoneID = [customZoneID: config]

operation.recordZoneIDs = [customZoneID]
operation.fetchAllChanges = true

operation.recordZoneChangeTokensUpdatedBlock = { _, changeToken, _ in
    // Store changeToken to be used in subsequent fetches.
}

operation.recordZoneFetchCompletionBlock = { [weak self] _, _, _, _, error in
    // Handle error if needed
}

operation.recordChangedBlock = { record in
    // Parse record and store it locally for later use
}

operation.recordWithIDWasDeletedBlock = { recordID, _ in
    // Delete the local entity represented by the record iD
}

operation.qualityOfService = .userInitiated
operation.database = privateDatabase

cloudOperationQueue.addOperation(operation)

Architecting for sync

If you’re working in an app that syncs private user data to CloudKit, you should strive for an offline-first architecture, so that a person can use your app normally even when there’s no internet connection available.

In order to achieve this, you should probably architect your code in such a way that sync is abstracted away from what your user touches. This means not firing CloudKit operations based on button taps in view controllers, for instance.

The exact shape of this will depend on which local storage solution you’re using. I have experience implementing CloudKit syncing with both Realm and CoreData, and in the sample app I provide with this article, I’ve used a simple plist file on disk to store the data locally.

The key is that your sync code should observe your local store for new records, updated records and deleted records. When changes are detected, they should be uploaded to CloudKit using a CKModifyRecordsOperation.

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 the retryAfterSeconds property. 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:

/// Retries a CloudKit operation if the error suggests it
///
/// - Parameters:
///   - log: The logger to use for logging information about the error handling, uses the default one if not set
///   - block: The block that will execute the operation later if it can be retried
/// - Returns: Whether or not it was possible to retry the operation
@discardableResult func retryCloudKitOperationIfPossible(_ log: OSLog? = nil, with block: @escaping () -> Void) -> Bool {
    let effectiveLog: OSLog = log ?? .default

    guard let effectiveError = self as? CKError else { return false }

    guard let retryDelay: Double = effectiveError.retryAfterSeconds else {
        os_log("Error is not recoverable", log: effectiveLog, type: .error)
        return false
    }

    os_log("Error is recoverable. Will retry after %{public}f seconds", log: effectiveLog, type: .error, retryDelay)

    DispatchQueue.main.asyncAfter(deadline: .now() + retryDelay) {
        block()
    }

    return true
}

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.

It’s probably a good idea to have a helper that can delegate conflict resolution to your model, through a closure:

/// Uses the `resolver` closure to resolve a conflict, returning the conflict-free record
///
/// - Parameter resolver: A closure that will receive the client record as the first param and the server record as the second param.
/// This closure is responsible for handling the conflict and returning the conflict-free record.
/// - Returns: The conflict-free record returned by `resolver`
func resolveConflict(with resolver: (CKRecord, CKRecord) -> CKRecord?) -> CKRecord? {
    guard let effectiveError = self as? CKError else {
        os_log("resolveConflict called on an error that was not a CKError. The error was %{public}@",
               log: .default,
               type: .fault,
               String(describing: self))
        return nil
    }

    guard effectiveError.code == .serverRecordChanged else {
        os_log("resolveConflict called on a CKError that was not a serverRecordChanged error. The error was %{public}@",
               log: .default,
               type: .fault,
               String(describing: effectiveError))
        return nil
    }

    guard let clientRecord = effectiveError.userInfo[CKRecordChangedErrorClientRecordKey] as? CKRecord else {
        os_log("Failed to obtain client record from serverRecordChanged error. The error was %{public}@",
               log: .default,
               type: .fault,
               String(describing: effectiveError))
        return nil
    }

    guard let serverRecord = effectiveError.userInfo[CKRecordChangedErrorServerRecordKey] as? CKRecord else {
        os_log("Failed to obtain server record from serverRecordChanged error. The error was %{public}@",
               log: .default,
               type: .fault,
               String(describing: effectiveError))
        return nil
    }

    return resolver(clientRecord, serverRecord)
}

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 to use it for your next project.

Check out this article’s companion project on my Github.

Further reading