Transcript
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
[Applause]
>> 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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
[Applause]
>> 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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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 developer.Apple.com/CloudKit.
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 CloudKit@Apple.com.
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.
[Applause]