MacKuba

Kuba Suder's blog on Mac & iOS development

CloudKit

App ClipsAppKitCloudKitExtensionsFoundationiCloudLocationLoggingMacMapsPerformancePhotosPrivacySafariSwiftSwiftUIUIKitWatchKitWWDC 12WWDC 14WWDC 15WWDC 16WWDC 18WWDC 19WWDC 20

View as index

CloudKit Best Practices

Categories: CloudKit, iCloud, WWDC 16 0 comments Watch the video

Short CloudKit overview

Apple uses CloudKit in their applications, so you can be confident that it scales, because for Apple it scales to hundreds of millions of users

CloudKit lets you focus on building your applications and not worry about building backend services for them

It provides your users automatic authentication – if the user is logged in to iCloud on their device, they don’t need to log in separately in your app

A CloudKit container now includes 3 databases:

  • public database for data visible to everyone
  • private database for a given user’s private data
  • new this year: shared database for user data that they decided to share with others

Zones:

  • public database has 1 default zone
  • private database has a default zone and it can have one or more custom zones
  • shared database includes some number of shared zones

A record always exists in a specific zone

Building an app with a sync feature

A common use case (e.g. Notes app):

  • user creates some data/records/documents on one of their devices
  • later, they open another device and they expect to see these documents there and be able to read/edit them

The way this is implemented is that CloudKit needs to be the source of truth, and the devices should maintain a local cache of all the app data and synchronize it using CloudKit

The recommended workflow:

  1. 1. On app launch, fetch changes from the server
  2. 2. Subscribe to any future changes
  3. 3. Fetch changes when you receive a push

Subscriptions:

Subscriptions let you ask the server to notify you whenever a change happens in the specified set of data. Previously you could subscribe to a specific query to a record type or to all changes in a zone.

New in iOS 10 – CKDatabaseSubscription – lets you subscribe to all changes in the whole database (private or shared).

Types of subscription notifications:

  1. 1. Silent push:
let notificationInfo = CKNotificationInfo()

// we only set this, but none of the UI related keys
notificationInfo.shouldSendContentAvailable = true

// do this once. no need to ask the user for push notifications permission,
// since we won't show any visible notifications
application.registerForRemoteNotifications(…)
  1. 2. Visual notification:
let notificationInfo = CKNotificationInfo()

// set any of these
notificationInfo.shouldBadge = true
notificationInfo.alertBody = "alertBody"
notificationInfo.soundName = "default"

// we need to prompt the user for push notification access:
application.registerUserNotificationSettings(…)
application.registerForRemoteNotifications(…)

Remember that push notifications can be coalesced, so you may only get one out of a series. Push notifications tell you that *something* has changed, but not necessarily every single thing that has changed.

Creating a subscription:

This only needs to be done the first time you launch an app – so we set a flag when we create a subscription and the next time we skip this part.

if subscriptionIsLocallyCached { return }

let subscription = CKDatabaseSubscription(subscriptionID: "shared-changes")

let notificationInfo = CKNotificationInfo()
notificationInfo.shouldSendContentAvailable = true
subscription.notificationInfo = notificationInfo

let operation = CKModifySubscriptionsOperation(
    subscriptionsToSave: [subscription],
    subscriptionIDsToDelete: []
)

operation.modifySubscriptionsCompletionBlock = { …
    if error != nil {
        …
    } else {
        self.subscriptionIsLocallyCached = true
    }
}

operation.qualityOfService = .utility
self.sharedDB.add(operation)

Listening for pushes:

  • turn on “Remote notifications” and “Background fetch” capabilities
func application(_ application: UIApplication,
    didReceiveRemoteNotification userInfo: [NSObject: AnyObject],
    fetchCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {

    let dict = userInfo as! [String: NSObject]
    let notification = CKNotification(fromRemoteNotificationDictionary: dict)

    if notification.subscriptionID == "shared-changes" {
        fetchSharedChanges {
              completionHandler(.newData)
        }
    }
}

Fetching the changes:

Steps:

  • ask in which zones something was changed (in shared db – because there may be new zones added when a new user shares some content)
  • ask which records have changed in each relevant zone

The server will not send you pushes about the changes you’re doing on this device, but you may receive those changes you’ve done on the list when fetching a delta download

fetchAllChanges: previously, in some operations you had to manually check for a flag that says there is more results waiting for you that you need to manually request (i.e. another page)

Now, CloudKit does the paging automatically for you if fetchAllChanges = true (which is the default)

func fetchSharedChanges(_ callback: () -> Void) {
    let changesOperation = CKFetchDatabaseChangesOperation(
        previousServerChangeToken: sharedDBChangeToken  // cached between runs
    )

    // this gives you IDs of changed zones
    changesOperation.recordZoneWithIDChangedBlock = { … }

    // this gives you IDs of deleted zones
    changesOperation.recordZoneWithIDWasDeletedBlock = { … }

    // this gives you the current change token which you need to save
    // may be called multiple times if the operation fetches multiple pages of content
    // save the token each time, so in case of an error you don"t repeat all work
    changesOperation.changeTokenUpdatedBlock = { … }

    changesOperation.fetchDatabaseChangesCompletionBlock = {
        (newToken: CKServerChangeToken?, more: Bool, error: NSError?) -> Void in

        self.sharedDBChangeToken = newToken
        self.fetchZoneChanges(callback)
    }

    self.sharedDB.add(operation)
}

fetchZoneChanges looks very similar, but fetches changes for a specific zone using CKFetchRecordZoneChangesOperation (you pass it a list of zones)

CloudKit best practices:

Automatic authentication:

CloudKit allows you to authenticate users (if they’re logged in to iCloud) without requiring any private information

You use the CloudKit user record for authentication

The user record is unique per container and never changes for that user

container.fetchUserRecordID(completionHandler: (CKRecordID?, NSError?) -> Void)

CKOperation API:

The convenience API works on single items and it’s simpler to use

Every convenience API call has a CKOperation counterpart that lets you perform an operation on a batch of records

The CKOperation also has other advantages – for example, it lets you:

  • set up dependencies between operations
  • specify quality of service and queue priorities
  • cancel operations that have started executing
  • specify if you want the operation to work over cellular network
  • limit the number of records or set of fetched keys
  • report progress
  • … and everything that NSOperation provides

(*) watch the Advanced NSOperations talk from 2015 to learn more about NSOperation

Quality of service:

QoS: select a quality of service (.userInteractive / .userInitiated / .utility / .background) depending on the task priority

  • default is .utility
  • .utility and below enable discretionary networking

Discretionary networking means that:

  • the system decides when is the best moment to run your request, so it may take longer than you expect
  • however, all network failures will be automatically retried for you
  • the request gets a timeout period of 7 days by default

Long lived operations:

If you have some operations that you want to continue/retry if they don’t manage to complete by the time your app is terminated, iOS 9.3 adds “CloudKit long lived operations”

Once you run such operation, the system will finish it even if the app is killed by the system or the user

The request is executed even if your app isn’t running, the result is cached and is returned to you once the app restarts

Results are kept by the OS for at least 24 hours

To use this API:

  • set isLongLived = true on CKOperation
  • save the operation’s operationID
  • use CKContainer.fetchLongLivedOperation(withId:) to get the operation object back
  • set completion blocks and run it again just like a new one
CKContainer.default().fetchLongLivedOperation(withID: myOpID) {
    (operation: CKOperation?, error: NSError?) in

    let fetchRecords = operation as! CKFetchRecordsOperation
    fetchRecords.fetchRecordsCompletionBlock = { … }

    CKContainer.default().privateCloudDatabase.add(fetchRecords)
}

Parent references:

A new type of reference added this year to help you better model data, especially with sharing in mind

If your app supports sharing, it’s recommended that you set the parent reference to create a hierarchy between records

Example: Album  ⭢  list of photos

let photoRecord = CKRecord(recordType: "photo")
photoRecord.setParent(albumRecordID)

What this gives you: when the user shares the album record, the whole record hierarchy under this album (photos and other data) will also be shared

Types of errors:

1) Fatal error (bad request)

Error codes like:

  • .internalError
  • .serverRejectedRequest
  • .invalidArguments
  • .permissionFailure

→ in this case, you should show an alert to the user and tell them this can’t be executed

2) Connection/server error

Error codes like:

  • .zoneBusy
  • .serviceUnavailable
  • .requestRateLimited

→ in this case, check for CKErrorRetryAfterKey and retry after specified time

3) Errors that are returned before connection is even made

.networkUnavailable

  • you should monitor network reachability (SCNetworkReachability) and retry when the device is connected again

.notAuthenticated

  • when the user is not logged in and can’t access their private database
  • you should register at startup for CKAccountChangedNotification, and when it fires, recheck account status and update the UI

CloudKit Tips and Tricks

Categories: CloudKit, iCloud, WWDC 15 0 comments Watch the video

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 save
  • CKRecordChangedErrorAncestorRecordKey – the original version
  • CKRecordChangedErrorServerRecordKey – 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 sortDescriptor 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:

We 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 on CKOperation 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

Advanced CloudKit

Categories: CloudKit, iCloud, WWDC 14 0 comments Watch the video

CloudKit API is designed to be asynchronous, all calls return through a callback, because they all require a network connection

The main API (“operational API”) is based on NSOperation

You use it by creating special NSOperation objects for a given use case, e.g. CKFetchRecordsOperation, and specifying parameters and callbacks in its properties

Apart from the final result callback, you can set callbacks e.g. for reporting download progress or to get records one by one as they’re downloaded

Operation lifecycle (cancelling, suspending etc.) can be managed through standard NSOperation methods and NSOperationQueue

There are separete fetch/modify operation types for records, subscriptions, zones, users and notifications

You can set dependencies between operations (also if they’re in different queues), e.g. make a fetch operation and then a modify operation that needs to wait for the object to load

Operations can also have different priority levels

Starting an operation:

(ℹ️ Note: this wasn’t in the video, but it really should have been, because it's completely not obvious.)

How to start an operation once you prepare the CKOperation object:

1) Use the database’s built-in queue:

let fetchOperation = ...
CKContainer.default().privateCloudDatabase.add(fetchOperation)

2) Use your own operation queue and assign a reference to the database:

let operationQueue = NSOperationQueue()

let fetchOperation = ...
fetchOperation.database = CKContainer.default().privateCloudDatabase
operationQueue.addOperation(fetchOperation)

Custom zones

Custom zones (in the private database) let you compartmentalize data and add some special features

Records can’t be moved between zones or have cross-zone relationships

There are some operations that can only be done in custom zones:

Atomic commits:

Objects in the CloudKit database have relationships between them, and you want to keep all data consistent

Atomic commits are kind of like transactions in a relational database: batch operations succeed or fail together

Only available in the private database (because public database may be accessed by millions of users at the same time)

If an operation fails, you get a CKErrorPartialFailure response, with the user info containing info about errors on specific records (CKPartialErrorsByItemID)

Error CKErrorBatchRequestFailed means that this record wasn’t saved because of a problem with another record in the batch

Delta downloads:

Allows you to download a list of all changes since the last time the app was online, to let you perform a full sync

When a device connects, you can send a “change token” to the server asking for all changes since that version

This lets you implement an offline cache of the whole dataset and sync any changes when possible

To do that:

  • track all local changes
  • send changes to the server when connected
  • resolve conflicts
  • fetch server changes with CKFetchRecordChangesOperation
  • remember the received new server change token and send it back next time

Zone subscriptions:

Lets you subscribe for notifications about any change in the zone

When you get a notification, you request a delta download

Advanced record operations

Record changes:

When you change some fields in a CKRecord, the changes are automatically tracked locally and only the changed fields are transmitted when you save it

By default CloudKit performs a “locked update”, which makes sure that the update is only saved on the server if the record wasn’t modified in the meantime by another client (this uses record change tokens)

After you execute a save, the server returns your record with a new change token – so you should use that returned version for any subsequent changes

Unlocked update  ⭢  just overwrites server data regardless what is there

Locked update  ⭢  if the record was changed in the meantime, you get back an error (CKErrorServerRecordChanged)

The userInfo of the CKErrorServerRecordChanged error contains info that lets you perform a 3-way merge:

  • CKRecordChangedErrorClientRecordKey – what you tried to save
  • CKRecordChangedErrorAncestorRecordKey – the original version
  • CKRecordChangedErrorServerRecordKey – what is currently on the server

Based on the values from these 3 copies of the record you can decide what state the record should be in, and then retry the save

You can modify the behavior with “save policies”:

  • SaveIfServerUnchanged  ⭢  default, performs a locked update and sends only changed keys
  • SaveChangedKeys  ⭢  unlocked update, sends only changed keys
  • SaveAllKeys  ⭢  unlocked update, overwrites all keys in the record (note: this doesn’t affect keys that aren’t present in the local copy at all)

You should almost always use the default locked update (SaveIfServerUnchanged), use unlocked updates only to forcefully resolve serious conflicts

Use SaveAllKeys if the user requests to overwrite server data with local data

Partial records:

The desiredKeys field present in most operation types lets you specify that you only want to download selected keys from the server

This is useful if the whole record is very large and you don’t need all of it

Partial records can be normally saved after a change

CloudKit data modeling

References:

Forward reference  ⭢  a parent object keeps an array of references to children in its property

Backward reference  ⭢  only child objects have a reference to the parent

It’s recommended to use backward references – with a forward reference you need to update the parent object every time a new child is added, and you will run into conflicts if multiple clients are adding records

To get a list of all children using backward references, make a query for all child records with a predicate “owner = X”

References give you cascading deletes – when you delete the parent object, all child objects and their children are deleted

If an object has two parent references, it’s deleted when the first parent is deleted

When batch uploading a tree of objects, CloudKit makes sure that parent objects are uploaded first so that you don’t get inconsistent data during upload (important in the public database)

Your data objects:

CloudKit is only a transport mechanism and requires you to keep and manage your own local copy of all data

It’s recommended that you don’t subclass CK* objects to build your models – make your own completely independent model classes and translate to/from CloudKit objects when fetching and saving

Handling push notifications:

You need to remember that push notifications in general aren’t guaranteed to be delivered

The server only stores one push per client, so if you reconnect e.g. after a flight, you might miss some previous notifications

You can find pushes that you’ve missed in a “Notification Collection” where every notification is saved

The Notification Collection works kind of like delta updates – you ask for notifications since a given change token and you get a list of everything added since then

You can mark a notification as read, which notifies all other clients that they can ignore it

You should check the Notification Collection every time you get a push, since you never know what you might have missed (this doesn’t only happen with airplane mode)

The iCloud Dashboard

The dashboard lets you browse data saved by your app – the whole public database and the private database for your developer account (but not anyone else’s private database)

You can view saved records, run queries with any filters, and add new records

You can define roles in the public database and define for each model who can create/read/modify records (e.g. specify that records are publicly readable but only an admin can create them)

You will also see a list of all user ids and first/last names of those users that marked themselves as discoverable

Schema:

The CloudKit database has two separate “environments”: development and production

The schema for each record type is “just in time” during development, i.e. when you save a new type of record, it automatically creates a new schema for that record type, recording every field type, and when you save a record with a new field, it adds a field to the list

However, once you’re ready to release a new version of your app, you need to save the schema to production and at that point it’s locked – a production version of the app can’t save records or fields that aren’t defined in the schema

CloudKit also automatically creates indexes for each field in each record type – when you’re done with development, you can delete some indexes that you won’t need so they don’t waste space in the production database

Tips & tricks

Please handle all errors :)

Remember that you can get partial errors (when atomic commits aren’t used), so some records might be saved while others aren’t

Retry any “server busy” errors (CKErrorRetryAfterKey tells you the amount of time you should wait)

Don’t waste space in your users’ iCloud in private databases, they may be paying real money for it

Limits in the public database are mostly to prevent abuse, they should be fine for most normal use (the limits scale with the number of users)

Introducing CloudKit

Categories: CloudKit, iCloud, WWDC 14 0 comments Watch the video

CloudKit lets you write client applications without having to build and host a server part to handle things like database, accounts or push notifications

Usage is free for the developer up to pretty big limits

CloudKit gives you more direct access to iCloud servers

It’s the framework that’s used behind the scenes by iCloud Photo Library and iCloud Drive

Uses the same iCloud account as iCloud documents or key-value storage

Two types of databases:

  • public – accessible to everyone
  • private – private data of a specific user

It’s only a transport technology, it does not deal with local data persistence – you need to decide how you store the data that you load from the cloud

To enable iCloud in your app, set it up in the Capabilities tab in Xcode just like with other iCloud APIs

Containers:

Each app’s data is kept in a separate container (CKContainer)

Containers give you the safety that your app’s data will not be mixed with someone else’s app’s data

A container’s ID needs to be unique in the whole iCloud, so use reverse-domain style identifiers

By default each app has one container of its own, but apps can additionally use shared containers

Containers are managed by the developer through the WWDR portal

Databases:

A container contains one shared public database for everyone, and separate private databases for each user

An app running on the device has access to one public and one private database (CKDatabase)

The database is the initial entry point to CloudKit (from a container)

CKDatabase *publicDatabase = [[CKContainer defaultContainer] publicCloudDatabase];
CKDatabase *privateDatabase = [[CKContainer defaultContainer] privateCloudDatabase];

Private database:

  • requires a logged in iCloud account
  • data stored counts against the user’s iCloud account quota
  • default permission for data is user readable
  • the data your users store in your app’s CloudKit container is *not* accessible to you

Public database:

  • can be accessed anonymously even if the user isn’t logged in
  • data stored counts against the developer’s app quota
  • default permission for data is world readable
  • permissions can be customized using iCloud Dashboard Roles

Records:

A record (CKRecord) is a single “object” in the CloudKit database, essentially a list of key-value pairs

Records have a Record Type (~ table name)

There is no defined up front schema, you can just save a record of any type with any keys and the schema will be updated based on that

→ note: this only works in development, the schema is fixed in production, see "Advanced CloudKit"

Records can have metadata: who created it & modified it and when, also includes a “change tag” (version id) – for determining if two sides have the same version of a record

Record values can be: strings, numbers, dates, NSData, CLLocation, CKReference, CKAsset, arrays of any of these

- (instancetype)initWithRecordType:(NSString *)recordType;
- (id)objectForKey:(NSString*)key;
- (void)setObject:(id)object forKey:(NSString *)key;
- (NSArray *)allKeys;

Subscripts also work:

CKRecord *party = [[CKRecord alloc] initWithRecordType:@"Party"];
party[@"start"] = [NSDate date];

Record zones:

Records are grouped within a database inside “zones” (CKRecordZoneID)

The public database has one zone, the private database has one default zone, but it can have additional custom zones

Record identifiers:

Record identifier (CKRecordID) is a tuple grouping: a “record name” + zone ID

You can provide a recordID when creating a record instance

If you don’t provide a recordID, a random UUID will be assigned

References:

A reference (CKReference) is a pointer from one record to another, as an id of the “parent” record contained in a child record’s field

References allow you to do cascade deletes, deleting child records when parent is deleted

You can create a reference from a CKRecord object or from a CKRecordID if you know the ID but don’t have the object in memory

Assets:

An asset (CKAsset) is an unstructured piece of data, basically a binary file

Assets are downloaded and uploaded from/to files on disk, not from memory

An asset is always owned by a record, and is deleted when the record is deleted

Transport of assets is optimized so that only the minimal amount of data is transferred

APIs:

There are two different APIs for managing CloudKit data: “operational API” and “convenience API”

The operational API has every possible operation you might need, the convenience API is more convenient

Start with the convenience API, use operational API for tweaking and overriding options if needed

CloudKit APIs for saving/fetching data are asynchronous – there is no SDK-managed local data, everything needs to go over the network unless you manually cache it

In CloudKit it’s absolutely necessary to properly handle error cases – every network call can fail and your app needs to be prepared for this

Convenience API:

[publicDatabase saveRecord:obj completionHandler: { … }];
[publicDatabase retchRecordWithID:recordID completionHandler: { … }];

Queries:

For any large database, or the shared public database, you shouldn’t try to keep a copy of the whole database on disk and sync all of it, but instead fetch what you need on demand – for this you can use queries

A query (CKQuery) allows you to fetch a list of records matching some conditions

Query can specify a RecordType, NSPredicate and optionally NSSortDescriptors

A subset of NSPredicate language is supported, if something is not supported you’ll get an exception

Predicates such as “equal”, “greater than”, “distance to location”, string tokenizing, and OR / AND are supported

[publicDatabase performQuery:query inZoneWithID:nil completionHandler: { … }];

Subscriptions:

If you repeatedly run the same query, polling for the same data, you can ask the server to run the query for you and notify you immediately when a new record is added

You do that by creating a subscription (CKSubscription)

A subscription includes: RecordType, NSPredicate and push configuration (CKNotificationInfo)

Your app is notified of changes through a push notification with some additional data

CKSubscription *subscription =
  [[CKSubscription alloc] initWithRecordType:@"Party"
                                   predicate:predicate
                                     options:CKSubscriptionOptionsFiresOnRecordCreation];

[publicDatabase saveSubscription:subscription completionHandler: { … }];

Pushes are handled through the usual push API:

-(void)application:(UIApplication *)application
didReceiveRemoteNotification:(NSDictionary *)userInfo;

Build a CKNotification object from the user info:

CKNotification *cloudKitNotification =
    [CKNotification notificationFromRemoteNotificationDictionary:userInfo];

NSString *alertBody = cloudKitNotification.alertBody;

if (cloudKitNotification.notificationType == CKNotificationTypeQuery) {
    CKQueryNotification *queryNotification = cloudKitNotification;
    CKRecordID *recordID = [queryNotification recordID];
}

Handling user accounts:

Your application does not get direct access to any user identifiers like iCloud email address

Instead, in each container each user is represented as a unique ID within that container that doesn’t change unless the user switches to another account

The same user will have a different ID in a different CloudKit container

The ID is an instance of CKRecordID

[[CKContainer defaultContainer] fetchUserRecordIDWithCompletionHandler: { … }];

Each user has a user record representing them (which is almost like any other record) with their user id and record type = CKRecordTypeUserRecord, one in the private database, and another with the same ID in the public database

You can set and read any key-value data on this record like on other records

However, these records aren’t created by you and can’t be queried to get a list of users

[publicDatabase fetchRecordWithID:userRecordID completionHandler: { … }];

User discovery:

You can ask the user to allow you to make them discoverable by other users (they get a request popup)

If they agree, they can be looked up by user ID, specific email, or by fetching a list of all users matching your user’s contacts from the address book (this doesn’t give your app access to the address book itself, just a list of matching users)

You get back record IDs, first & last names of users, but no emails

[defaultContainer discoverAllContactUserInfosWithCompletionHandler: { … }];

Returns an array of CKDiscoveredUserInfo objects with properties userRecordID, firstName, lastName

When to use CloudKit vs. other APIs?

CloudKit doesn’t replace or deprecate any existing iCloud APIs [yet ;P], it’s just an additional tool

Key-value store:

  • asynchronous, small amounts of data
  • mostly for application preferences

iCloud Drive:

  • works on files and folders
  • on OSX it makes a full offline cache of the drive
  • good for document-centric apps

iCloud Core Data:

  • built on top of iCloud Drive
  • good for keeping private, structured data (custom databases) in sync
  • note: the whole data set is downloaded to each device

CloudKit:

  • good for sharing public data between users, both structured data and large files
  • good for large data sets where not every device needs to have a copy of the whole database
  • for attaching some data to the user’s identity and sharing info between users that know each other
  • more low-level, your app is in control of when any information is downloaded or uploaded to the iCloud servers, and has responsibility for handling sync