BY Paul Young - Freelance Sofware Engineer
These days, modern mobile application development requires a well thought-out plan for keeping user data in sync across various devices. This is a thorny problem with many gotchas and pitfalls, but users expect the feature, and expect it to work well.
For iOS and macOS, Apple provides a robust toolkit, called CloudKit API, which allows developers targeting Apple platforms to solve this synchronization problem.
In this article, I’ll demonstrate how to use CloudKit to keep a user’s data in sync between multiple clients. It’s intended for experienced iOS developers who are already familiar with Apple’s frameworks and with Swift. I’m going to take a fairly deep technical dive into the CloudKit API to explore ways you can leverage this technology to make awesome multi-device apps. I’ll focus on an iOS application, but the same approach can be used for macOS clients as well.
Our example use case is a simple note application with just a single note, for illustration purposes. Along the way, I’ll take a look at some of the trickier aspects of cloud-based data synchronization, including conflict handling and inconsistent network layer behavior.
What is CloudKit?
CloudKit is built on top of Apple’s iCloud service. It’s fair to say iCloud got off to a bit of a rocky start. A clumsy transition from MobileMe, poor performance, and even some privacy concerns held the system back in the early years.
For app developers, the situation was even worse. Before CloudKit, inconsistent behavior and weak debugging tools made it almost impossible to deliver a top quality product using the first generation iCloud APIs.
Over time, however, Apple has addressed these issues. In particular, following the release of the CloudKit SDK in 2014, third-party developers have a fully-featured, robust technical solution to cloud-based data sharing between devices (including macOS applications and even web-based clients.)
Since CloudKit is deeply tied to Apple’s operating systems and devices, it’s not suitable for applications that require a broader range of device support, such as Android or Windows clients. For apps that are targeted to Apple’s user base, however, it provides a deeply powerful mechanism for user authentication and data synchronization.
Basic CloudKit Setup
CloudKit organizes data via a hierarchy of classes:
At the top level is
CKContainer, which encapsulates a set of related CloudKit data. Every app automatically gets a default
CKContainer, and a group of apps can share a custom
CKContainer if permission settings allow. That can enable some interesting cross-application workflows.
CKContainer are multiple instances of
CKDatabase. CloudKit automatically configures every CloudKit-enabled app out of the box to have a public
CKDatabase (all users of the app can see everything) and a private
CKDatabase (each user sees only their own data). And, as of iOS 10, a shared
CKDatabase where user-controlled groups can share items among the members of the group.
CKRecordZones, and within zones
CKRecords. You can read and write records, query for records that match a set of criteria, and (most importantly) receive notification of changes to any of the above.
For your Note app, you can use the default container. Within this container, you’re going to use the private database (because you want the user’s note to be seen only by that user) and within the private database, you’re going to use a custom record zone, which enables notification of specific record changes.
The Note will be stored as a single
modified (DateTime), and
version fields. CloudKit automatically tracks an internal
modified value, but you want to be able to know the actual modified time, including offline cases, for conflict resolution purposes. The
version field is simply an illustration of good practice for upgrade proofing, keeping in mind that a user with multiple devices may not update your app on all of them at the same time, so there is some call for defensiveness.
Building the Note App
I’m assuming you have a good handle on the basics of creating iOS apps in Xcode. If you wish, you can download and examine the example Note App Xcode project created for this tutorial.
For our purposes, a single view application containing a
UITextView with the
ViewController as its delegate will suffice. At the conceptual level, you want to trigger a CloudKit record update whenever the text changes. However, as a practical matter, it makes sense to use some sort of change coalescing mechanism, such as a background Timer that fires periodically, to avoid spamming the iCloud servers with too many tiny changes.
CloudKit app require a few items to be enabled on the Capabilities Pane of the Xcode Target: iCloud (naturally), including the CloudKit checkbox, Push Notifications, and Background Modes (specifically, remote notifications).
For the CloudKit functionality, I’ve broken things into two classes: A lower level
CloudKitNoteDatabase singleton and a higher level
But first, a quick discussion of CloudKit Errors.
Careful error handling is absolutely essential for a CloudKit client.
Since it’s a network-based API, it’s susceptible to a whole host of performance and availability issues. Also, the service itself must protect against a range of potential issues, such as unauthorized requests, conflicting changes, and the like.
CloudKit provides a full range of error codedes, with accompanying information, to allow developers to handle various edge cases and, where necessary, provide detailed explanations to the user about possible issues.
Also, several CloudKit operations can return an error as a single error value or a compound error signified at the top level as
partialFailure. It comes with a Dictionary of contained
CKErrors that deserve more careful inspection to figure out what exactly happened during a compound operation.
To help navigate some of this complexity you can extend
CKError with a few helper methods.
Please note all the code has explanatory comments at the key points.
Apple provides two levels of functionality in the CloudKit SDK: High level “convenience” functions, such as
delete(), and lower level operation constructs with cumbersome names, such as
The convenience API is much more accessible, while the operation approach can be a bit intimidating. However, Apple strongly urges developers to use the operations rather than the convenience methods.
CloudKit operations provide superior control over the details of how CloudKit does its work and, perhaps more importantly, really force the developer to think carefully about network behaviors central to everything CloudKit does. For these reasons, I am using the operations in these code examples.
Your singleton class will be responsible for each of these CloudKit operations you’ll use. In fact, in a sense, you’re recreating the convenience APIs. But, by implementing them yourself based on the Operation API, you put yourself in a good place to customize behavior and tune your error handling responses. For example, if you want to extend this app to handle multiple Notes rather than just one, you could do so more readily (and with higher resulting performance) than if you’d just used Apple’s convenience APIs.
Creating a Custom Zone
CloudKit automatically creates a default zone for the private database. However, you can get more functionality if you use a custom zone, most notably, support for fetching incremental record changes.
Since this is a first example of using an operation, here are a couple of general observations:
First, all CloudKit operations have custom completion closures (and many have intermediate closures, depending on the operation). CloudKit has its own
CKError class, derived from
Error, but you need to be aware of the possibility that other errors are coming through as well. Finally, one of the most important aspects of any operation is the
qualityOfService value. Due to network latency, airplane mode, and such, CloudKit will internally handle retries and such for operations at a
qualityOfService of “utility” or lower. Depending on the context, you may wish to assign a higher
qualityOfService and handle these situations yourself.
Once set up, operations are passed to the
CKDatabase object, where they’ll be executed on a background thread.
Creating a Subscription
Subscriptions are one of the most valuable CloudKit features. They build on Apple’s notification infrastructure to allow various clients to get push notifications when certain CloudKit changes occur. These can be normal push notifications familiar to iOS users (such as sound, banner, or badge), or in CloudKit, they can be a special class of notification called silent pushes. These silent pushes happen entirely without user visibility or interaction, and as a result, don’t require the user to enable push notification for your app, saving you many potential user-experience headaches as an app developer.
The way to enable these silent notifications is to set the
shouldSendContentAvailable property on the
CKNotificationInfo instance, while leaving all of the traditional notification settings (
soundName, and so on) unset.
Note also, I am using a
CKQuerySubscription with a very simple “always true” predicate to watch for changes on the one (and only) Note record. In a more sophisticated application, you may wish to take advantage of the predicate to narrow the scope of a particular
CKQuerySubscription, and you may wish to review the other subscription types available under CloudKit, such as
Finally, observe that you can use a
UserDefaults cached value to avoid unnecessarily saving the subscription more than once. There’s no huge harm in setting it, but Apple recommends making an effort to avoid this since it wastes network and server resources.
Fetching a record by name is very straightforward. You can think of the name as the primary key of the record in a simple database sense (names must be unique, for example). The actual
CKRecordID is a bit more complicated in that it includes the
CKFetchRecordsOperation operates on one or more records at a time. In this example, there’s just the one record, but for future expandability, this is a great potential performance benefit.
Saving records is, perhaps, the most complicated operation. The simple act of writing a record to the database is straightforward enough, but in my example, with multiple clients, this is where you’ll face the potential issue of handling a conflict when multiple clients attempt to write to the server concurrently. Thankfully, CloudKit is explicitly designed to handle this condition. It rejects specific requests with enough error context in the response to allow each client to make a local, enlightened decision about how to resolve the conflict.
Although this adds complexity to the client, it’s ultimately a far better solution than having Apple come up with one of a few server-side mechanisms for conflict resolution.
The app designer is always in the best position to define rules for these situations, which can include everything from context-aware automatic merging to user-directed resolution instructions. I am not going to get very fancy in my example; I am using the
modified field to declare that the most recent update wins. This might not always be the best outcome for professional apps, but it’s not bad for a first rule and, for this purpose, serves to illustrate the mechanism by which CloudKit passes conflict information back to the client.
Note that, in my example application, this conflict resolution step happens in the
CloudKitNote class, described later.
Handling Notification of Updated Records
CloudKit Notifications provide the means to find out when records have been updated by another client. However, network conditions and performance constraints can cause individual notifications to be dropped, or multiple notifications to intentionally coalesce into a single client notification. Since CloudKit’s notifications are built on top of the iOS notification system, you have to be on the lookout for these conditions.
However, CloudKit gives you the tools you need for this.
Rather than relying on individual notifications to give you detailed knowledge of what change an individual notification represents, you use a notification to simply indicate that something has changed, and then you can ask CloudKit what’s changed since the last time you asked. In my example, I do this by using
CKServerChangeTokens. Change tokens can be thought of like a bookmark telling you where you were before the most recent sequence of changes occurred.
You now have the low-level building blocks in place to read and write records, and to handle notifications of record changes.
Let’s look now at a layer built on top of that to manage these operations in the context of a specific Note.
For starters, a few custom errors can be defined to shield the client from the internals of CloudKit, and a simple delegate protocol can inform the client of remote updates to the underlying Note data.
CKRecord to Note
In Swift, individual fields on a
CKRecord can be accessed via the subscript operator. The values all conform to
CKRecordValue, but these, in turn, are always one of a specific subset of familiar data types:
NSDate, and so on.
Also, CloudKit provides a specific record type for “large” binary objects. No specific cutoff point is specified (a maximum of 1MB total is recommended for each
CKRecord), but as a rule of thumb, just about anything that feels like an independent item (an image, a sound, a blob of text) rather than as a database field should probably be stored as a
CKAsset. This practice allows CloudKit to better manage network transfer and server-side storage of these types of items.
For this example, you’ll use
CKAsset to store the note text.
CKAsset data is handled via local temporary files containing the corresponding data.
Loading a Note
Loading a note is very straightforward. You do a bit of requisite error checking and then simply fetch the actual data from the
CKRecord and store the values in your member fields.
Saving a Note and Resolving Potential Conflict
There are a couple of special situations to be aware of when you save a note.
First off, you need to make sure you’re starting from a valid
CKRecord. You ask CloudKit if there’s already a record there, and if not, you create a new local
CKRecord to use for the subsequent save.
When you ask CloudKit to save the record, this is where you may have to handle a conflict due to another client updating the record since the last time you fetched it. In anticipation of this, split the save function into two steps. The first step does a one-time setup in preparation for writing the record, and the second step passes the assembled record down to the singleton
CloudKitNoteDatabase class. This second step may be repeated in the case of a conflict.
In the event of a conflict, CloudKit gives you, in the returned
CKError, three full
CKRecords to work with:
- The prior version of the record you tried to save,
- The exact version of the record you tried to save,
- The version held by the server at the time you submitted the request.
By looking at the
modified fields of these records, you can decide which record occurred first, and therefore which data to keep. If necessary, you then pass the updated server record to CloudKit to write the new record. Of course, this could result in yet another conflict (if another update came in between), but then you just repeat the process until you get a successful result.
In this simple Note application, with a single user switching between devices, you’re not likely to see too many conflicts in a “live concurrency” sense. However, such conflicts can arise from other circumstances. For example, a user may have made edits on one device while in airplane mode, and then absent-mindedly made different edits on another device before turning airplane mode off on the first device.
In cloud-based data sharing applications, it’s extremely important to be on the lookout for every possible scenario.
Handling Notification of a Remotely Changed Note
When a notification comes in that a record has changed,
CloudKitNoteDatabase will do the heavy lifting of fetching the changes from CloudKit. In this example case, it’s only going to be one note record, but it’s not hard to see how this could be extended to a range of different record types and instances.
For example purposes, I included a basic sanity check to make sure I am updating the correct record, and then update the fields and notify the delegate that we have new data.
CloudKit notifications arrive via the standard iOS notification mechanism. Thus, your
AppDelegate should call
didFinishLaunchingWithOptions and implement
didReceiveRemoteNotification. When the app receives a notification, check that it corresponds to the subscription you created, and if so, pass it down to the
Tip: Since push notifications aren’t fully supported in the iOS simulator, you will want to work with physical iOS devices during development and testing of the CloudKit notification feature. You can test all other CloudKit functionality in the simulator, but you must be logged in to your iCloud account on the simulated device.
There you go! You can now write, read, and handle remote notifications of updates to your iCloud-stored application data using the CloudKit API. More importantly, you have a foundation for adding more advanced CloudKit functionality.
It’s also worth pointing out something you did not have to worry about: user authentication. Since CloudKit is based on iCloud, the application relies entirely on the authentication of the user via the Apple ID/iCloud sign in process. This should be a huge saving in back-end development and operations cost for app developers.
Handling the Offline Case
It may be tempting to think that the above is a completely robust data sharing solution, but it’s not quite that simple.
Implicit in all of this is that CloudKit may not always be available. Users may not be signed in, they may have disabled CloudKit for the app, they may be in airplane mode—the list of exceptions goes on. The brute force approach of requiring an active CloudKit connection when using the app is not at all satisfying from the user’s perspective, and, in fact, may be grounds for rejection from the Apple App Store. So, an offline mode must be carefully considered.
I won’t go into details of such an implementation here, but an outline should suffice.
The same note fields for text and modified datetime can be stored locally in a file via
NSKeyedArchiver or the like, and the UI can provide near full functionality based on this local copy. It is also possible to serialize
CKRecords directly to and from local storage. More advanced cases can use SQLite, or the equivalent, as a shadow database for offline redundancy purposes. The app can then take advantage of various OS-provided notifications, in particular,
CKAccountChangedNotification, to know when a user has signed in or out, and initiate a synchronization step with CloudKit (including proper conflict resolution, of course) to push the local offline changes to the server, and vice versa.
Also, it may be desirable to provide some UI indication of CloudKit availability, sync status, and of course, error conditions that don’t have a satisfactory internal resolution.
CloudKit Solves The Synchronization Problem
In this article, I’ve explored the core CloudKit API mechanism for keeping data in sync between multiple iOS clients.
Note that the same code will work for macOS clients as well, with slight adjustments for differences in how notifications work on that platform.
CloudKit provides much more functionality on top of this, especially for sophisticated data models, public sharing, advanced user notification features, and more.
Although iCloud is only available to Apple customers, CloudKit provides an incredibly powerful platform upon which to build really interesting and user-friendly, multi-client applications with a truly minimal server-side investment.
To dig deeper into CloudKit, I strongly recommend taking the time to view the various CloudKit presentations from each of the last few WWDCs and follow along with the examples they provide.
For original article, click here.