WWDC 15
CloudKit Tips and Tricks
Error Handling
Accounts:
To check the account status of the current user:
container.accountStatusWithCompletionHandler { status, error in … }
All APIs that fail because they require an authenticated user return CKErrorNotAuthenticated
You can now subscribe for CKAccountChangedNotification
to be notified when account status changes
You should avoid showing alerts to the user about a missing account – simply disable parts of the UI that require an account, and reenable them when you get a notification that an account is now available
Network errors:
Network connection errors that you may sometimes get: CKErrorNetworkFailure
, CKErrorServiceUnavailable
, CKErrorZoneBusy
, CKErrorRequestRateLimited
These errors include a key CKErrorRetryAfterKey
in their user info dictionary that tells you how long you should wait before retrying
Handling conflicts:
If you try to save a record that has been modified in the meantime on the server (meaning: the record change tag you’re sending with the save request is outdated), you will receive the error CKErrorServerRecordChanged
There is no magic happening behind the scenes in such case, iCloud doesn’t make assumptions about how you want to resolve conflicts, you need to handle this yourself
However, the SDK provides you the necessary information in the userInfo:
CKRecordChangedErrorClientRecordKey
– what you tried to saveCKRecordChangedErrorAncestorRecordKey
– the original versionCKRecordChangedErrorServerRecordKey
– what is currently on the server
Usually you will want to resolve the conflict by applying the same changes that you did on the original record to the current server version of the record
CloudKit Operations
Batch operations:
If you create and save a lot of records in one go, each of them will create a separate network request, and making a lot of requests in a short period means you’re likely to hit some kind of rate limit and they will be queued up
To avoid making multiple similar requests, you can use the CKOperation
API
Almost every convenience API method that works on one record at a time has a CKOperation
counterpart that allows you to work on a batch of records
For saving multiple records, use CKModifyRecordsOperation
:
class CKModifyRecordsOperation: CKDatabaseOperation { convenience init(recordsToSave: [CKRecord]?, recordIDsToDelete: [CKRecordID]?) }
Note: there are certain limits on how large batch operations you can make (number of items in the request and total request size)
This doesn’t include the size of saved binary assets, just the record field data
If you hit this limit, you will get the CKErrorLimitExceeded
error
In that case, the best solution is usually to try to divide the batch in half and make two requests
If one or more records in the batch can’t be saved, you will get CKErrorPartialFailure
From this error’s userInfo
you can get a dictionary with specific record errors under CKPartialErrorsByItemIDKey
In a standard zone, in such scenario some records will be saved and those with errors won’t
In a custom zone you can make an atomic update – in this case, in case of a problem with some of the records, no records will actually be saved, but instead you will get an error CKErrorBatchRequestFailed
for those that could have been saved but weren’t
Queries:
If you expect a query to return a large number of records, but you only need a small number of them at a time, you can use the CKQueryOperation.resultsLimit
property
Also available on CKFetchRecordChangesOperation
, CKFetchNotificationChangesOperation
When limiting the number of records, you will usually also want to set the query's sortDescriptors
to e.g. sort records by oldest or newest first
You can use the creationDate
key which is automatically added to all saved records regardless of type
To implement pagination and get further pages beyond the first one, use the CKQueryCursor
object that you get in response to the queryCompletionBlock
callback
Then, initialize the next CKOperation
passing it the cursor object in the argument to the initializer
If you don’t need all information contained in a record immediately, you can also use the desiredKeys
property to only download the keys you want
E.g. download the record’s thumbnail image but not a full-size photo
Also available on CKFetchRecordsOperation
, CKFetchRecordChangesOperation
Maintaining a local cache:
Let's say you want to keep some subset of all data completely cached on all local devices for quicker access (e.g. user’s personal notes)
You have two options:
- use
CKQueryOperation
to fetch all records and synchronize them manually - make a custom zone and use delta downloads using
CKFetchRecordChangesOperation
When saving fetched records to a local database, you should save CKRecord’s system fields like the change tag together with your own data
To do that, you can use encodeSystemFieldsWithCoder:
let data = NSMutableData() let archiver = NSKeyedArchiver(forWritingWithMutableData: data) archiver.requiresSecureCoding = true record.encodeSystemFieldsWithCoder(archiver) archiver.finishEncoding()
When restoring a record from the local storage, you don’t have to set all its data fields – it’s fine to only set those you want to change
To synchronize any changes from the server, create a subscription subscribing to a given record type using silent notifications, and use CKFetchRecordChangesOperation
to fetch all recent changes when notified
Subscriptions (CKSubscription
) are persistent queries on the server that send remote notifications about a relevant change – either in a specific record set (query subscription) or in the whole zone (zone subscription)
To get CloudKit subscription notifications, you need to follow the usual setup for push notifications:
- have push notification capability enabled for your app
- call
registerForRemoteNotifications()
- call
registerUserNotificationSettings(…)
if you want to show notifications to the user
To ask for silent subscription notifications, configure the CKNotificationInfo
object appropriately:
- set the
shouldSendContentAvailable
key - do not set any of the UI-related keys:
alertBody
,shouldBadge
,soundName
Notification priorities: a notification is high priority if it has any UI keys set, otherwise it’s medium priority
For silent notifications, the UIApplicationDelegate
will receive the following callback:
func application(application: UIApplication, didReceiveRemoteNotification: [NSObject: AnyObject], fetchCompletionHandler: (UIBackgroundFetchResult) -> Void) { … }
Remember that push notification delivery in general is “best effort” – pushes can be dropped if many are received in a short period of time or because of network issues
Silent notifications may also be additionally delayed if the system is waiting for better conditions
When you receive a notification, use CKFetchNotificationChangesOperation
to check the server’s notification collection for any notifications you might have missed
You may want to use a UIApplication
background task (beginBackgroundTaskWithName(…)
) for syncing tasks
Interactive notifications: you can now make CloudKit notifications interactive (e.g. show action buttons) by setting the category
key on CKNotificationInfo
Other performance tips
CloudKit is a highly asynchronous API, most operations require a network call and take some time to execute
You will often want to make a series of operations that have some dependencies between them
Things to keep in mind:
- however you implement task handling, remember to always handle all errors
- never block the main thread with an operation in progress
Don’t nest calls to the convenience API methods, creating a “callback hell”
Don’t use locks/semaphores to wait for an API call to finish
Instead, use the addDependency()
API in CKOperation
to add dependencies between operations:
let firstFetch = CKFetchRecordsOperation(…) let secondFetch = CKFetchRecordsOperation(…) secondFetch.addDependency(firstFetch) let queue = NSOperationQueue() queue.addOperations([firstFetch, secondFetch], waitUntilFinished: false)
Use the qualityOfService
property on NSOperation
to indicate which operations are something you need in the UI and which are low priority background operations
- there used to be a
usesBackgroundSession
property onCKOperation
too, but it’s deprecated now ⭢ use quality of service for this
QoS = .utility
and .background
use discretionary networking, use .userInteractive
and .userInitiated
for high priority tasks
Note: .background
QoS is the default if you don’t change it!
- update: now it's
.utility