CloudKit Tips and Tricks

Session 715 WWDC 2015

CloudKit makes it easy to store and retrieve any kind of data from iCloud. Dive into the API with the CloudKit framework team as they explore some of its lesser-known features, explore best practices around subscriptions and queries, and reveal its hidden gems.


NIHAR SHARMA: Good afternoon and welcome to the CloudKit tips and tricks session.

My name is Nihar Sharma and I'm an engineer on the CloudKit team.

I know some of you may be completely new to our platform and encountering the CloudKit framework for the first time while others may already have an app on the Store.

This session we will have something for everyone.

Let's jump right in.

What is CloudKit?

Last year we introduced CloudKit as a whole new way for you to be able to talk to Apple's iCloud database servers.

With that we gave you a set of built-in technologies like large file storage.

We gave you a privacy conscious identifier to be able to manage the users who could now be anyone with an iCloud account.

First and foremost, we made this public facing developer API because we wanted you to be able to leverage the power of this platform and build great apps for your users.

Last but not the least, Apple's heavily invested in this technology.

Last year alone when we first shipped, we shipped with a couple of major clients including iCloud drive and iCloud photo library, and this year we added a host of new clients like the Notes app, the news app and WWDC app a lot of you have been using throughout the week and have in your hands right now.

If all of this sounds unfamiliar to you, I invite you to go back and take a look at the intro to CloudKit and Advanced CloudKit sessions from last year's conference.

They are a great resource for an introduction to the new API, and I highly recommend you check them out.

First things first.

A lot of you have been playing with the amazing new features of Swift 2.

I'm pleased to announce with iOS 9, the experience of using CloudKit from Swift is much better.

Let me give you a couple of examples of what I'm talking about.

Up until now, you had to use the old set object for key, object for key syntax when setting and getting values in the CK record, which are the workhorse of the CloudKit API, but with iOS 9, you can use the much more familiar and modern dictionary subscripting syntax when working with CK records.

In addition to that, we have made your CloudKit code from Swift as well as Objective-C a lot more type safe by adopting nullability qualifiers and Lightweight generics.

Previously you could have set an array of objects of any type on the records and safe product of CKRecords operation.

Now with the latest tools in iOS 9, the compiler can warn you when you do that so you can catch the errors early and write more robust code.

So with that, let me give you a brief recap on the storage architecture of CloudKit.

The top level silo in CloudKit is called a CloudKit container.

It is subdivided into two databases.

The public database, which is a large soup of all of your apps data shared among all of your users, and the private database which is tied to a user's iCloud account.

This database will contain data for a particular iCloud account shared across all of that user's devices.

Within each database we have a further layer of isolation for the records you store in them, and we call these record zones.

They are a way for CloudKit to offer additional capabilities for the records you store in them where we can.

If the public database has a single zone called the default zone where all the records live, and the private database also has one default zone.

Along with that, we give you the capability to create multiple custom zones where you have these additional capabilities for your records.

So with that, let's talk about what we will cover in our session today.

You might remember the schema from last year.

We talked about an example schema for an app that shows parties with clouds.

It had a simple schema where we had a party record type and clown record type and stored them in the public database.

I thought this year let's run with this example and develop a couple of features for this app, example app we'll call clown central because it's all about clowns.

We will use the example to walk through a set of tips and tricks that you can use when working with CloudKit.

Our app will have a simplistic UI where we show the list of parties and a couple of features that we will walk through together in this session.

Now, through this example, there are four major areas I want to cover today.

Number one is error handling.

Last year we told you a difference between a CloudKit app that handles errors and one that does not is not the difference between a great app and a good one.

It's a difference between a functional app and a completely broken one.

We meant it.

I would like to walk you through a set of special error codes you may encounter when using the API and give you some general guidelines on how to handle them.

With that, we'll start talking about a couple of tips that you can keep in mind when maintaining a local cache when working with CloudKit.

That will lead us into talking about how to get set up with subscriptions to keep our cache up to date, and finally I would like to talk about a set of general purpose performance tips that you should keep in mind and adopt in your apps today.

So we've got a ton of great stuff to cover.

Let's jump in and talk about error handling.

The first thing that I would like to do is talk about accounts.

CloudKit does not require you to have an iCloud account to be used.

We allow anonymous read only access to the public database.

Let's say for demonstration purposes here that the clown central app will require an iCloud account.

We talk about a couple of features that use the private database which, by definition, require an authenticated account.

And by default, write access to the database requires an account as well.

As a reminder, the way you check the account status for the current user is by using the account status with completion handler API, available on CKContainer.

Any errors that you encounter when working with CloudKit due to authentication will fail with a special error code called CKErrorNotAuthenticated.

The general guideline we give to handle this error is to recheck the account status.

Let's say we have a missing iCloud account.

When you check the account status, you receive CKAccountStatusNoAccount.

Previously you had no way of knowing when requests that failed due to a missing account would start succeeding again.

For that very purpose with iOS 9 and OS X El Capitan, we added CKAccountChangeNotification.

We will send you this notification whenever there is a change to the user's account, for example on log ins, log outs or if the iCloud drive capability switch is turned on or off.

With that I would like to touch on a couple of best practices when handling a missing account in your apps.

It might be tempting when encountering this situation to throw up an alert for the user telling them they don't have a logged in iCloud account and can't proceed.

This is not helpful to the user because they might dismiss the alert and retry an operation that led them to see the alert in the first place.

What we recommend instead is that you gracefully degrade your UI in a way that simply disables the features of your app that require an account, and for this purpose you can now use CKAccountChangedNotification to re-enable that UI, when you receive it, re-check the account status, and see that one account is now available.

A missing account is not one of the only conditions under which your operations might fail temporarily, but may start succeeding at some point in the future.

For example, under poor network conditions where you might encounter this error, CKErrorNetworkFailure, or if the CloudKit servers are busy.

Or you might see one of these errors: CKErrorServiceUnavailable or CKErrorZoneBusy.

When encountering this error, we want you to retry the operation at a later date, but you might be wondering when do I retry those operations?

Well, you don't have to guess at that value.

In these errors, user info dictionaries we return to you a special value under the key "CKErrorRetryAfterKey."

This value is a value of time in seconds that you need to wait before retrying that operation.

Now, let's take a similar example where let's say our app initially had a bug which might have caused it to send a lot of updates to the server in a very short amount of time.

Let's say if this app made it to the wild in that way and a lot of users started hitting that bug, it would overwhelm the iCloud servers.

The way we avoid this is by using a special error code called CKErrorRequestRateLimited.

This is CloudKit's way of mitigating application bugs from overwhelming the iCloud servers.

Any requests that hit the rate limited error will not be sent up to the server until a period of time has elapsed.

Once again what is that period of time?

It is given to you by the CKErrorRetryAfterKey.

So when you encounter this error, look for this key in the errors user info dictionary.

Wait for a period of time, and retry your request.

Now I would like to start talking about a different class of errors that you might encounter because of the way your schema is designed, specifically if your schema allows multiple users to update the same record in your Cloud database.

So let's say we want to add a feature to our app where we allow attendees to add themselves to a party.

But unfortunately, when designing the schema for this feature, we did not watch last year's advanced CloudKit session.

So this is a schema we came up with.

On the party record itself, we decided to store an array of references to attendee records that want to join that party.

Now, you can see that every single time we wish to add an attendee to a particular party, we are going to end up modifying the same party record.

Let's take a look at an example of what happens when two different users try to add themselves to a party.

Since the WWDC bash is starting up soon, let's say we saved this record to CloudKit.

And now before we get into what happens when two users download this record, I would like to talk about what are record change tags.

You can think of them as simply a string that the server uses to identify a particular version of a record.

This version of the record as it exists on the server is recognized by the change tag A.

We expose this to you as a read only property on CKRecords, but it will only be populated on records that have been saved.

Let's say two users, John and Alice, come along and download this particular version of the record.

You can see they receive the same change tags, A.

Now, John adds himself as an attendee to the party first, goes ahead and tries to save his record to the server.

Now, with the records saved, we will send the change tag that John had, which is A, up to the server.

And the server sees that the change tags match and accepts John's modification.

Now, since the version of the server record has changed, the server will generate a new change tag in this case, B, and send that back to John in the record save response.

Now let's say Alice comes along and decides to attend the party.

She tries the same operation, adds herself to the array and tries to save her version of the record.

This time you can see that she will be sending up the old change tag A, and the server will complain that she is trying to alter a version of the server record that no longer exists.

She encountered a conflict.

On her device, the way CloudKit tells her about this conflict is by a special error code called CKErrorServerRecordChanged.

There's no magic happening behind the scenes, and we don't make assumptions about how you wish to resolve conflicts.

You are the best person to do that.

So we will try to provide you with as much useful information as we can for you to resolve those conflicts yourself.

And the first and most important piece of information that we give you is the version of the record as it was in the server when an update was rejected.

Where do you find that?

You find that once again in the errors user's info dictionary under the key CKRecordChangedErrorServerRecordKey.

In this case, when we pull it out of the errors user's info dictionary, we would find the record as it was in the server with John attending the party and the new change tag B.

Now, in addition to the server record, we give you back a few more pieces of information.

These include the ancestor record key which is the record as Alice had before she made any modifications to it.

And the client record key which will contain the record that Alice tried to save to the server.

Now, what I want to emphasize here is that the most important thing to do, and what you will be doing in most cases when resolving a conflict, is trying to save the modifications that you were in the first place before you encountered the error but instead on to the server record returned to you by the error.

So in this case, you take the server record.

We'll make the same modification to it that we were trying to save, which in our case is simply add Alice as an attendee to the party, include her along with John, and save this version of the record to the server.

You can see that we have the server's new change tag B.

When we save the record those change tags will match, and the server will accept the save.

Now, a point to note here is that we could have avoided this entire class of errors if we had used a better schema for this feature.

I'll talk about what that schema is in a short while.

But you can see that trying to modify the same record every single time a different user makes that modification is not the best idea.

So talk through this new schema, let's look at CloudKit operations.

We want to add a feature to our app that allows users to store photos for parties.

We need a similar one-to-many relationship between parties and photos this time, so photos would be their own record type, but we don't want to store them on the party record this time.

How do we do this?

We can save the photo records with a back reference to the party that they belong to instead.

You can see now when we save photo records, we don't have to modify the party record that they belong to.

So let's talk about how we are saving these records.

Let's say right now in our app we are using the convenience API, saveRecordWithCompletionHandler to save one photo record at a time.

But users could potentially store multiple photos at once.

In that case, we are currently using the convenience API in the tight loop to save multiple records.

Let's take a look at what is happening behind the scenes when we do that.

The app calls the convenience API a bunch of times to be able to save multiple photos.

Each one much those in the system gets wrapped into a CKOperation with a set of default values, and each one of those operations turns into at least one network request when we try to save that record up to the server.

We are not going to overwhelm the server with all of those requests at once, so we have also created a bottleneck in the system, and the system sends up a few requests at a time in order to save those records.

Now, in addition to this bottleneck, there is one more thing that you should consider.

Every single one of those requests to save one record at a time, for example in this case, counts against your network request quota as CloudKit app developers.

This is clearly a bad idea.

We want to be able to batch those record updates into one network request, or at least the minimum number of network requests possible.

How do we do that?

Well, we do that by using the CKOperation counterpart to our convenience API.

Almost every convenience API that works on one item at a time has a CKOperation counterpart that batches record updates together.

In this case we want to use CKModifyRecordsOperation to be able to save multiple records at once by providing them as an array to the record save property.

Look at what happens when we adopt this operation.

Now we can bunch all of the records that we want to save into one operation.

It queues in the system.

The system is able to use the minimum number of requests it needs to be able to save those records to the server, and we eliminated the bottleneck.

At the same time we've helped you optimize the use of your request quota.

This is an important point I would like all of you to think about in your apps when using the convenience API.

If you are ever using it for the same kind of request in multiple places or some kind of loop, think instead of adopting the CKOperation API that lets you batch those updates.

It will save you your request quota, and at the same time be more efficient for the system.

All right.

Now that we are working with batches, there is an additional consideration that we need to think about.

The server imposes certain limits on the sizes of the batches that can be sent up at once.

These limits include the number of items in each request, as well as the total size of the request.

The total size of the request is simply the sum of the key value data that you set in the records that that belong to that request.

An impportant thing to keep in mind here is that the size of the data the you are trying to store as part of bulk stoarage via the CKAsset API does not count towards this key value data.

But if your request were to trip any one of these limits, you would receive a special error code called CKErrorLimitExceeded.

The general guideline we give developers to handle this error is simply divide the number of items in your batch by half and issue two operations instead of one.

And recursively do that if those operations encounter the same error again.

Now, what if only some items in your batch were to fail?

Since the batch consists of a lot of items but returns only one error to you, we still want to tell you about every single one of those errors.

We do that by using a special error code called CKErrorPartialFailure.

This is a top level error code that you don't really want to handle directly, but once again under the errors user's info dictionary, if you look under CKPartialErrorsByItemIDKey, we will give you a dictionary of item IDs to the corresponding errors from your batch.

For example, in this case we have had one item ID that failed with CKRecord invalid arguments, and there may or may not be errors for any other items in your batch.

You want to open this up, look inside the dictionary and handle that error individually.

This situation changes slightly when considering atomic updates in custom zones.

Custom zones, as a reminder, have the capability for your CKModifyRecordsOperation to issue atomic updates, in which case the server will either accept the entire batch as one or fail the entire batch.

Now, if one item in our batch, as in this case, would have failed with CKError invalid arguments, the rest of the item IDs would also contain an errror with a special error code, CKErrorBatchRequestFailed.

When working with atomic updates, make sure to look inside the dictionaries and handle all the errors that are not in CKErrorBatchRequestFailed.

That's storing all of our photo records up to the Cloud in an optimized manner.

Let's talk about the other half, downloading them.

The way we do that is by using CloudKit queries.

All right.

Downloading photo records for a particular party has now become really easy with the new schema that we adopted where photo records reference the party they belong to.

We do that by simply constructing a CK query that tries to match that reference to a known party record ID.

Now, when we issue our query to download photos for a party, some parties might have a lot of photos.

Do we really need to download all of them?

Let's take a look at how we can issue an optimized download for the photos for a particular party by using CKQueryOperation.

The first question to really answer is: We have no idea how many photos belong to a particular party.

So how many should we download?

It doesn't make sense to download all of them.

What makes sense is for our UI to drive the answer to that question.

Now, if you take a look at our example UI here, you can see that when we pull up a particular party, all we see are 20 photos.

So it would make a lot of sense if our query only returned 20 photos to us when we first issued it.

We can do just that by using the results limit property on CKQueryOperation.

This property helps a lot for you to be able to manage items in a particular batch size when you have no idea how many items might be returned to you in total.

So for that reason it is also available on CKFetchRecordChangesOperation where you maybe returned a lot of changes, and you have no idea how many from a custom zone and on CKFetch notification changes operation for a similar reason.

All right.

So now we are downloading just 20 records.

That's an improvement.

But can we do better?

Well, let's take a look at what we are downloading.

Once again we let our UI answer this question for us.

Whenever we are viewing a particular party, all we are seeing is tiny thumbnails, cropped and down scaled photos for a particular party.

But what we've stored on our photo record that is being downloaded completely by default for us is probably a high resolution version of that photo that we've taken with the amazing cameras on our iOS devices.

Well, wouldn't it be great if we could somehow add that information right on to our photo record so that we have something that we can pull down partially, but how do we pull down partial records?

Well, we do that by using the desired keys property on CKQuery operation.

In this case, the desired keys property will take an array of keys that you wish to fetch on all the records that match your query.

So if we set that just to be our photo thumbnail, you can see that we have drastically reduced the amount of data that we are loading when our query returns.

This is also available on CKFetch records operation where you may know the record IDs in advance of the records you are fetching, but either your UI or some other reason you only want partial records to be downloaded.

As well as on CKFetch record changes operation which once again by default downloads the full record for any records that may have changed.

So now that we're displaying only 20 photos, it makes sense for us to have a certain ordering on the photos that we are first displaying to the user.

Let's say we want to show the photos in the order that they have been, in the order that they were most recently saved into iCloud.

We do that by setting the sort discriptor right on the CKQuery that we initialized our CKQuery operation with.

You can see here we are creating a sort descriptor on the creation date key which is a system field on all CKRecords that have been saved to the server.

And set that to descending.

One thing to keep in mind here, since this is a system field, you need to ensure that it's sortable on the server.

You do that configuration via the iCloud dashboard.

Make sure to have that configuration set before those records are saved.

Otherwise, the previous records saved previously are not going to have that index on them.

So now that we are fetching just a small slice of our entire results set, you may be wondering, how do we show the user the rest?

Say the user starts scrolling down, and we want to look at the next batch of photos.

How do we implement pagination in this case?

Well, we do that by looking at what we get back in our query completion block.

When a query completes, in addition to all the results that were returned to us in the progress call backs, we get back a CKQuery cursor.

This is an opaque marker for you to use that shows you your place in the entire results set.

So you should store the query cursor returned to you from the first query operation, and when you wish to fetch the next batch of results, initialize another CKQuery operation using the cursor initializer and pass it, the cursor that you stored previously.

Now, since we are optimizing our CKQuery operation in this manner, make sure to set the same desired keys and results limit on the new query operation once again.

That will give you just the optimized next batch of photos.

That was about downloading records.

Now I would like to switch gears and talk about some tips that you can keep in mind when maintaining a local cache working with CloudKit.

Let's start talking about a new feature.

Let's say we want to add the ability for users to store small personal Notes for parties.

Now, since these Notes are going to be personal, we want to store them in the user's private database.

We don't want to fetch these Notes every single time a user wants to view them or modify them.

We want to make sure that we have some kind of offline access for these Notes.

And you can see that in this particular scenario what we actually need is a small amount of data but on all of a particular user's devices.

So it makes a lot of sense for us to maintain a local cache when working with CloudKit in this scenario.

Let's first talk about how we can start downloading things from a private database.

Now, if you recall, we have the ability to store custom zones in the private database that gave us additional capabilities.

We go ahead and do just that.

Create a new zone in the private database called the notes zone.

And now we have two main ways in which we can start fetching data from this zone.

Once again, we can either use a CKQuery operation and optimize it just the way we saw, or we can use delta downloads via the CKRecords fetch operation which lets us fetch only the records in the zone that have changed.

If you recall, this operation is only available to work on zones which have the fetch changes capability.

Currently, all custom zones in a private database do have this capability.

Now, if you wish to learn more about how exactly delta downloads work, I invite you to go back and look at the advanced CloudKit session from last year.

It's a great walk through of how the operation exactly works.

Let's say we are using it.

We've started fetching our changes.

We have our app objects that we are storing in some sort of local database, whether it's core data or any other database of your choice.

That's where we encode our app objects currently.

So here we have a party object.

We see we've added the notes key on it corresponding for that particular user's Notes for that party.

We encode our app object to our local storage.

When working with a corresponding CKRecords, we want to store those records up in the Cloud.

We might think about encoding the entire CKRecord so that we have that cached along with our app object.

Let's take a look at what happens here.

You can see that CKRecord also has all of the app objects key set on it.

Of course, when we encode it we are duplicating all of the apps keys.

Once we encoded our app object and now when we are encoding our CKRecord.

This is clearly not what we want.

Well, the orange fields that you saw on the CKRecord belong just to the CKRecord.

They are the fields needed by the server to recognize a particular version of the record.

We call them system fields.

So what you really want in this case is a way to encode just the system fields of the record.

And you can do just that by using the encode system fields with coder API on CKRecord.

Now this is all the code that you need to be able to encode those system fields.

I highly recommend that you reference this if you ever, if the situation arises and you ever need to look back.

Now let's take a look at what happens when we start encoding just the system fields.

We are efficiently storing now what is important about a CKRecord and the corresponding party object.

Now, let's walk through a scenario of what happens when we try to modify a party object for which we've stored the system fields in this manner.

For that we use the coder initializer for CKRecord.

So you can see that when we pull it out, we will get back all the system fields that we had stored.

For brevity I've only shown the record ID and the change tag which we've already seen.

Now on this bare CKRecord it is completely legitimate for you to set just the keys that have changed on this record.

So let's say we want to change just the party and make this record our WWDC bash record.

We set the new value for that key and save the new record for the server.

It is important to note that you don't always have to set all of the keys that belong to a record when storing changes for that record.

So now that we are officially maintaining and storing that local cache, let's talk about how do we fetch changes from our custom zone in order to keep that cache up to date?

Well, once again we already have the answer to this by using CKFetch record changes operation which gives us all of the records that have changed in our zone.

The real question is, when do we use this operation?

Because using this operation alone does not tell us when our zone has changed.

So for that we need to use notifications via the CKSubscription API.

More specifically, since the changes in the zone are not changes that you wish to alert a user about, what we really want here are silent notifications.

So in the next section, I would like to talk to you about how to get up and running with subscriptions especially when you want to use silent subscriptions.

Let's start with brief recap.

What are subscriptions?

Subscriptions are per user persistent queries that you saved to the server.

They are a way for you, for your app to receive remote notifications per relevant changes.

There are two types of subscriptions, and they differ in the way you define what a relevant change for you is.

Number one, there are query subscriptions which allow you to store a predicate.

So when the predicate values to true, that's your relevant change.

The second ones are zone subscriptions where every modification to a zone counts as a relevant change.

So this is clearly what we want in the case of trying to get silent notifications whenever our zone changes.

But first, let's walk through the general setup that you need when handling all kinds of CloudKit subscriptions.

What I would like to emphasize with this setup is that you still need to go through the motions of setting up remote notifications as if they were not coming from CloudKit.

Let me show you what I mean by that.

Number one, you still need the APS capability for the app ID turned on from the developer portal.

This should get automatically turned on for you when you turn on the CloudKit capability.

Number two, you need to set the APS environment key in your app into a P list for development while you're testing your app and expecting remote notifications.

Third, you still need to register via the UI application API.

At the very least, you need to call register for remote notifications and also call user, register user notification settings if you are planning to show user notifications in your app.

Now, since we are interested in silent notifications and we're dealing with the CloudKit server that sends us notifications, how do we tell the server that this should be a silent notification?

We do that through CKNotification info corresponding to our CK subscription.

That is our entry point into telling the CloudKit server just what kind of a push payload should be sent and at what priority.

Let's talk about priorities.

So like I said, we configure our CKNotification info in a way that tells the CloudKit server that this is a silent notification and it needs to come at a low priority.

The server will send you a high priority push if you have any of these keys set on your CK notification info.

Whether it's the alert body, should badge or sound name.

These are what we call UI keys for your subscription.

If you send any one of them, the server sends a high priority push that is meant to be delivered immediately.

All other pushes are sent at medium priority and count as silent notifications.

So let's walk through what is a silent notification specific setup that you need.

Number one, you need to turn on the remote notification background mode for your app.

You do this through the capabilities pane in Xcode.

You should remember to checkmark that.

Number two, you should make sure that you implement the application that you receive remote notification, fetch completion handler notification of the application delegate API.

The other version is not going to be called in the background.

Make sure when you are expecting silent notifications, you have implemented this version.

And third, once again now we need to tell the CloudKit server that this is going to be a silent push, how do we configure our CKNotification info?

First and most importantly, you should set the 'should send content available' property to true.

It tells the CloudKit server that in your push payload it should include the content available key.

Secondly you should not set any of the UI keys that we just talked about on that CKNotification info.

Setting any one of these properties along with should send content available is not a supported configuration and will result in an error on the server.

So now let's talk about silent push delivery.

We've configured everything, we are expecting pushes.

When do we get them?

Since these notifications are not meant to alert the user in any way, they are sent at a time that is opportune for the system.

The system considers a variety of factors when deciding when they should get delivered.

And push delivery in general is best effort.

What I mean by that is that pushes could get coalesced or even dropped depending on the conditions of a device.

For example, if a device was in airplane mode when a flurry of pushes was expected, coming out of airplane mode, the Apple push notification server will only send the device the last push that was meant to be received on it.

Now, we have ways to mitigate this because we are dealing with CloudKit notifications.

In particular, CloudKit server stores all of the notifications that were meant to be delivered to your device in what we call a notification collection.

So when you do receive a silent notification, you should make sure to fetch changes from this notification collection, and you do that via the CKFetch notification changes operation.

So now we are getting silent notifications, we are checking if there are any notifications that we've missed, and we know that our zone has changed which is the reason we got the notification in the first place.

This is where we use CKFetch record changes operation to see what has changed in our zone.

But once again like we've talked about before, we have no idea how many things changed in that zone.

So potentially this could be a long running operation.

If you need a little more time for that operation to complete, I recommend that you look into the background task API on UI application.

This will let you get that extra time in order for your operation to complete.

Now, before we start talking about notifications, in iOS 8 we introduced an entirely new category of notifications called interactive notifications which allow a user to interact with pushes from banner, alert or from a notification center.

And we have had a lot of requests from you to be able to configure interactive notifications with CloudKit.

I'm pleased to announce with iOS 9 you can do just that with minimal amount of setup.

Once again if you just set the new category property on CKNotification info, it corresponds to the identifier you that registered with UI mutable notification categories when registering user notification settings.

That is all the setup you need to get up and running with interactive notifications with CloudKit.


NIHAR SHARMA: Thank you.

And with that, I would like to start talking about a general set of performance tips that you should keep in mind and use in your apps today when working with CloudKit.

CloudKit is a highly asynchronous API.

Most operations talk over the network, and it is very common to run into situations where you have a set of dependent tasks and you want to maintain some sort of ordering in which they complete.

Now, when implementing task management for these, there are a couple of goals, a couple of high level goals that we would like you to keep in mind.

Number one, obviously that whatever technique you employ allows you to implement great error handling for every single one of your CloudKit tasks.

Secondly, since these are asynchronous operations, you should make sure to never end up in a situation where you block the main thread and degrade their UI performance.

And last but not the least, as developers you want to make sure that your task management scheme is, lets you end up with maintainable code that is easy to reason about, debug and extend as you add new features to your app.

Let's take a look at a couple of ways where we do this and some dos and don'ts.

The number one don't is nesting convenience API calls.

Let's take a simple example.

If we had to modify one of the attendee records in the old schema that we saw, once again never use that schema in the real world.

But if you had to modify the attendee record, this is what you would have to do when using convenience API calls.

You would first fetch record with ID and try to fetch the party record that you know the attendees are part of, then pull out the record ID for the attendee from the attendees array, and then make your modification to the attendee's record, and then try to save that record.

This is trying to modify one record with one set of dependencies.

You can see we have ended up with code that is just a mangled piece of soup where you have no idea where to handle which error and how best to retry those operations.

In addition to that, there is an additional point of concern here.

Let's say that we issue these operations due to some sort of user action in our app.

Now, if a subsequent user action were to render these tasks unnecessary, once you've enqueued them, you have no way to cancel these tasks.

So if they are potentially long running, you are stuck with them running, you are stuck waiting for them.

We recommend that you never use this approach when managing dependent tasks especially if you need to make the same modification for a batch of records.

Now, another technique that we see is to simply get rid of the asynchronous nature of the API perhaps by introducing a semaphore and waiting on it.

This can get hairy in a couple of situations too.

You should almost never try to do this.

If you do, you should keep in mind that especially if you wait forever for operations to complete, it it is very easy for you to end up with circular dependencies that end up causing a deadlock in your app.

Or if you were to ever use this practice on a main thread, this will block your UI right away on an operation that is likely waiting on the network and result in a terrible user experience.

So we don't really recommend it.

What we do recommend is for you to take a look at the dependency management API that NSOperation offers.

This is what I mean by that API.

NSOperation lets you easily add and remove dependencies between other NSOperations.

Let's take a look at how this works with CKOperations that are a subclass of NSOperations.

If we have two dependent fetch records operations and the second one should not begin before the first one is completed, all you need to do is set up both of those operations and add the first fetch as a dependency on the second and enqueue both of those operations.

This will guarantee that the second fetch does not start before the first fetch is finished.

You can see this offers you a logical way to think about the errors for particular operations and at the same time manage dependencies for them in a convenient manner.

Now, when thinking about NSOperations from a performance context, there is an additional distinction that I would like you guys to think about.

Not all NSOperations are created equal.

Some of them may have been created because of an explicit user action in your apps while others may represent background tasks that are of lower priority.

To indicate this notion of relative importance between NSOperations to the system, in iOS 8 we introduced the quality of service property on NSOperations.

This property lets you indicate the nature and importance of work encapsulated by your NSOperation.

These are the various service levels that this property can take, and I recommend that you check out the documentation for a description of each one of these values and their significance.

But what is important to keep in mind here is that each of these service values directly affects the priority with which the NSOperation is allocated system resources, like CPU time, disk resources, as well as network resources.

Now, with CloudKit last year, we wanted to give you a similar way to be able to opt your lower priority CKOperations into discretionary network behavior.

What we mean by that, is that for your nonuser initiated tasks, for example, pre-fetching content for the user like we just went through by using CKRecords fetch record changes operation in response to silent notifications.

You want those tasks to opt into discretionary behavior so that the system waits for an opportune time to perform those network requests.

The system takes a variety of factors into account when deciding when to perform them, for exmple, cellular connectivity.

The system might wait for network connectivity to improve before sending out those requests.

Also power conditions.

If a user is running low on battery or the device is not currently charging, the system will wait for power conditions to improve before sending those requests.

We did this by exposing the user background session property on CKOperations.

With iOS 9 we saw an opportunity here to greatly simplify and unify these things, these two concepts by using quality of service to infer your network behavior, and at the same time pull in everything else that a given service level already indicates to the system.

So we are doing just that.

By deprecating the user's background session property and recommending that you start setting quality of service on all of your CKOperations.

Now, in the context of network behavior, you can set either of the service levels user interactive or user initiated to opt out of this discretionary behavior.

And for discretionary behavior, you can either set the value utility in which case we will try to infer whether you should be opted into discretionary based on whether the requesting app is foreground or not or background which will always result in discretionary network behavior.

Please keep in mind that if you build your apps with iOS 9 and OS X El Capitan or later, all new CKOperations will have the background quality of service by default.

You should make sure that you audit all of your CKOperations, take a look at what is the importance of work that they represent.

Be a good systems citizen and set the appropriate QS values on them.

NSOperation is very powerful API, and there's a lot more you can do with it.

If you want to learn more, I highly recommend that you go to the advanced NSOperation session tomorrow morning in Presidio.

In summary, I'd like to reiterate that error handling for your CloudKit code is vital.

It is as important as any feature, and we would like you to take a look, go back today and take a look at all your operations, see what kinds of errors have you been hitting, and if you followed the general guidelines we talked about today in handling them.

Number two, start batching your requests.

Whenever you see your app using the convenience API, working on one item at a time and doing that in multiple places, think with about using the CKOperation version of that API and batching those requests up.

You will not only improve the efficiency that your operations execute with in the system in general, you will also save your own network request quota.

Think about schema tradeoffs.

We've seen two cases where our schema tradeoffs came let us take advantage of optimizations.

For example when we added the thumbnail key to our photo record, we were able to optimize our download by just downloading the data that we need.

And in another case we were able to avoid an entire class of errors when we avoided the same party record being modified when photo records were stored on it.

So think about your schema carefully when designing features.

And last but not the least, configure your CKOperations.

They have, they are a very powerful API and they offer a ton of optimizations you can make to the actual network request that gets sent to the CloudKit servers.

For more information please check out our documentation on

For all the other questions and answers, the technical support, the forums and the CK support site are a great place.

For general queries, please e-mail

We have had some great related sessions this week.

I invite you to check them out when you go back today to Learn all that's new with Web services and what else is new in CloudKit.

We have one more Lab coming up tomorrow morning at 9 in Frameworks lab D.

Bring your questions and we'd be happy to answer them.

Thank you.


Apple, Inc. AAPL
1 Infinite Loop Cupertino CA 95014 US