As I Learn CloudKit Syncing - Part 3: Operational Approach

There are two sides to the CloudKit API: convienence and operations. The convienence API is there to quickly get up and running with CRUD, meant to work on single items like saving a CKRecord or fetching one. Even though the convienence API can make it easy to get started quickly, it can get messy:

Implementing CloudKit syncing using the “Tiny Data, All Devices” strategy as I wrote earlier necessitates the use of the other side of the CloudKit API: operations. More specifically, NSOperation and NSOperationQueue.

So in preperation for this feature, I did some homework. I read the Concurrency Programming Guide, the NSOperation reference, and the NSOperationQueue reference.

I also watched Advanced NSOperations talk from this years WWDC and my mind was opened to the power of NSOperation1.

If you haven’t watched that talk yet, please stop reading now and watch it, and then download the sample code2.

Advanced Operations and CKRecordZone

The key part of the CloudKit service that makes this syncing solution possible is the use of a custom CKRecordZone. A CKRecordZone object “defines an area for organizing related records in a database”. The public and private databases each have a default zone, and custom zones can be created as needed. Custom zones have two extra bits of functionality that are crucial for syncing: atomic commits and the ability to fetch only the changes that have occurred since the last fetch.

So I’m going to need to create a CKRecordZone before I can sync the game data to that zone. But before I can create a CKRecordZone I need to make sure I have permission to access iCloud. I could use the convienence API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[self.container accountStatusWithCompletionHandler:^(CKAccountStatus accountStatus, NSError *error) {

    if (accountStatus == CKAccountStatusAvailable) {

        CKRecordZone *newZone = [[CKRecordZone alloc] initWithZoneName:@"SyncZone"];

        [self.container.privateCloudDatabase saveRecordZone:newZone completionHandler:^(CKRecordZone *zone, NSError *zoneError) {

            if (!zoneError) {
                // Start syncing
            }

        }];
    }
}];

This… smells. It works until things start going wrong and those errors stop being nil. Being able to gracefully recover from errors, as the CloudKit team has said, isn’t the difference between a good app and a great app. It’s the difference between a non-functioning app and a functioning one.

So I think I can do better. And that “better” comes from using the operation API and some of the techniques found in the Advanced NSOperations talk.

Operation Dependencies

An operation is a discrete unit-of-work. Using NSOperation dependencies, you can make sure an operation only performs work if another operation finishes successfully.

The high level dependency graph of the above code looks a bit like this:

 ┌────────────────┐ 
 │                │ 
 │ Account Status │ 
 │                │ 
 └────────────────┘ 
          ▲         
          │         
          │         
┌──────────────────┐
│                  │
│Create Record Zone│
│                  │
└──────────────────┘
          ▲         
          │         
          │         
 ┌────────────────┐ 
 │                │ 
 │      Sync      │ 
 │                │ 
 └────────────────┘ 

Sync will only execute if Create Record Zone is successful. Create Record Zone will only execute if the Account Status check is a success.

This smells better. In the Create Record Zone operation, if CloudKit returns an error, I’ll have a chance to recover. For example, if I get back the CKErrorRequestRateLimited error, I can grab the CKErrorRetryAfterKey value from the error’s userInfo dictionary and schedule an attempt to retry the operation. If the retry is successful, the Create Record Zone operation completes and the Sync operation is then executed.

So that’s the high-level overview of my approach to building a gracefully recovering CloudKit syncing solution. In the next post I’m going to cover more specifics of the implementation.

Be notified of new posts by following me on Twitter.


  1. And I’m not the only one

  2. The first version of the sample code has a few bugs in it but Dave DeLong is working on a fix