WWDC 14
Introducing CloudKit
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
[note: this API has been replaced since then with a new one that returns CKUserIdentity
objects]
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