WWDC2013 Session 222

Transcript

X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
[ Silence ]
>> Good afternoon, and welcome.
Thank you.
How's everybody doing?
It's been a fun week,
excellent, good.
I'm Gordie Freedman.
I work on UIKit, and
today we're going to talk
about State restoration on iOS.
So has this happened to you?
You're using an application,
you go to do something else,
and when you go back, the
application's lost your place.
Maybe you were typing something,
maybe you were reading something
and scrolled somewhere, but
the application went back
to the beginning.
In fact, maybe you don't really
remember what you were doing.
You were hoping the
application would remember.
And as developers, we
don't want this to happen
to the users of our apps.
It can be very frustrating.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
But the good news is we've
got some really nice APIs
that make it very easy to
provide a seamless experience
when applications start back up.
I'm going to talk about
four things today.
First I'm going to go over a
small recap of the feature.
It's not a full review.
We did a session last year in
2012, and if you want to fill
in some of the gaps,
you can go watch that.
We also have some
documentation online,
and you can check that out.
But I think even if you're not
that familiar, you'll be able
to follow along just fine.
I'm going to focus a lot
on what's new in iOS 7.
We'll see what we've
added and how that plays.
We're going to talk a
little bit about security
and background operation --
couple of interesting topics
that I think we might want
to spend a little
time thinking about.
And then finally, I'm going
to go over some of the tools
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
that we have that can help you
to be able to both validate
that you're doing -- or your
app is doing what you expect,
and help diagnose problems.
So when we talk about
state restoration,
it's a simple idea --
one application to just go
back exactly where it was
so the user's experience
is not interrupted,
as if the application
had just been
in the background all the time.
And if the application exits and
restarts, the user won't know.
And it really is predicated
on how the user views this,
and we want to think a lot
about what the user's doing.
You want to look at
the different parts
of your application and
examine what you need to save
and what you need to restore.
And that's great, but
start at the top down.
Think about it as if you're
using the application and think,
"If I was using this and it
went away, where do I want it
to come back," and
then from there,
let that inform what
you save and restore.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
I'm going to give a
short demo just to kind
of highlight what
I'm talking about.
So I've got a very
simple application,
got a collection
view, you can see,
do the nice collection
view layout stuff.
And I'm going to
scroll around and look
for an image that I want to see.
So these are all thumbnails
with little titles --
pretty standard stuff.
I'm going to select one --
you can see that I've got
the selection there --
let it roll in.
Let's make it a little
bigger so it's easier to see.
Maybe I'll even get rid of these
bars here and make it like that.
So as a user you might
find an interesting picture
and then you want to
go show it to somebody,
or the phone rings,
or something happens.
The application goes
into the background.
And the application can exit
if left alone for awhile
for various reasons, so I'm
going to force that here.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Now, I just want to
point out one thing.
Ordinarily when a user goes into
the switcher and flips an app
up like I just did,
it'll get rid
of all the state
restoration information.
We do that on purpose
to kind of save a user
if every time they run the app,
it starts up in a bad
state and they're stuck.
First thing a frustrated
user might do is go
in and flick that up.
We'll get rid of the state,
give them a clean
start if that happens.
Hopefully it won't,
but it's an out.
But for developers, sometimes
you don't want that to happen.
If you're trying to chase
a bug, you're trying
to diagnose something
or just simply testing,
it's very convenient to go in,
kill the app and restart it.
You don't want to lose the info.
And I'll talk about that a
little more later, but I'm using
that trick here just to make
the demo a little easier.
So the app's not running
now, and if the user walked
up to their friend
and said, "Hey,
look at this cool picture,"
well, let's see what happens
if we start it back up.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
And it goes right
back to where it was.
It remembered that we weren't
showing any of the bars,
remembered the size of the
image, and even some things
that are behind the scenes.
Let's go here, I'm just
going to peel it back
with our cool new
gesture, and you can see
that it remembered
the scrollPosition
and the selected cell.
So that's a pretty
unexciting demo,
but it was supposed
to be actually.
The idea is it's predictable,
the users get what they want,
and it's nice and
easy for everybody.
So let's look at what we did
in order to accomplish that.
So first, what was
actually restored?
We saw it from the user's
perspective, but as a developer,
what were the constituent
pieces?
We had a navigation controller.
It remembered what
was pushed on it.
We also remembered
the scrollPosition
in our collection view
and the selected cell.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
And finally, we remembered
what image we were showing,
the transformation we
applied to resize it.
So you'll find that when you
write state restoration code,
a lot of what you do is map what
your application has already
done into what you need
to save and restore.
Typically, you'll find
that there's a lot
of really strong parallels
between code you've
already written, ad in fact,
you'll be able to
leverage and reuse most
of the code you've written.
So here, let's see what happens
before we even saved state
when the user selected
that cell and we pushed
on that image view controller.
If we didn't take any action
and we just loaded
the view controller,
we wouldn't have an
image or a title.
We want to put something there,
and we also want to remember
that we're going to change
that title up top, too.
So what do we do?
Well, we have a method called
prepare for segway that you use
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
when you have storyboards.
So that's what I have
in the example here.
And then prepare for segway,
I hand the view controller
that's coming in,
our image view controller,
enough information
so that it can get the
image and set that title.
So now when I did that
segway, we actually have
that information right there.
So similarly, when I save and
restoreState, I'm going to want
to be able to maintain that.
So what do we do?
Well, let's look at
how we saved state.
When the application
goes in the background,
we're going to go all the view
controllers with identifiers,
all the view controllers that
need to save their state,
and give them a chance.
So we're going to call them
method encodeRestorableState
with coder.
We're going to hand
a key to archiver
where it can stash any
information it wants
to restore later.
So what do we need to save here?
Well, we've got the
image, also that title.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
But we don't want to write
out the bits of the image
and actually save
an extra image file.
And in fact, if you think about
it, you probably have one place
that informs you of both the
image itself and that title.
So let's just save an identifier
that we can use later to look
up this information again.
And that as simple as
just encoding something
into that archiver that's
passed into the method.
And you can see here my image ID
is a string, so I'm just going
to encode an object,
give it a key
so I can look it back up later.
Anything else?
Well, we also remember whether
or not the user was showing the
bars on the top and the bottom.
So how do we save that?
Just as simple.
Just going to encode
a Boolean here,
and that's all we have to do.
So you'll notice, I'm not
showing a lot of code right now,
and there's a couple
reasons for that.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
One is, what's important is to
try to get the concepts across,
so you can see what it is that
we're actually trying to do.
If I show you the code, it's
easy to kind of get lost
in the forest just
looking at it.
Now, we have released the code,
so you'll be able to check
out this whole example and
see everything I'm talking
about here in more detail, but
you can do it on your own time.
So how did we know to
call encodeRestorableState
for that view controller object?
Well, here's all
the view controllers
in my application so far.
We've got the navigation
controller, that collection view
with all the little
images, the thumbnails.
What we just looked at was
the image view controller,
and of course there's some
views that go along with those.
What you do is you give them
restoration identifiers.
That tells us that you want us
to keep track of those objects
when we save state and
when we restore it.
And we will also call the
encodeRestorableState method
on all of them so that
we save everything.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Okay. So how about
when we restoreState?
So at this point, the
application is starting back up.
Do you need to do
anything different
when an application starts up
when you're restoring state
versus the very first time
the application launches?
Well, when an application
launches, we tell the delegate
that we're starting up, and here
we just simply call or recall,
the method application Will
finishLaunching with options.
And you don't have to do
anything different here
in the state restoration
case; just proceed as normal.
Set up your base interface, will
either load your base interface
from a storyboard or a nib,
or you can execute
whatever code you have,
get your window visible,
and you're good to go,
and that's our starting point.
Then we're going to
go and we're going
to call the restorationMethods
for the objects
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
that are participating.
So in this case, let's look
at the image controller.
So here, you can see we're
starting off without our image.
How do we get it back?
We have a complimentary method,
decodeRestorableStateWithCoder.
It's kind of the analog to
the one I just showed you,
where you saved your
state, fairly logical.
So what we're going to
do is we're going to pull
in that image identifier,
which informs us both
of the image and the title.
You can see that I got that.
Anything else?
Well, how did we get it?
Similar to the way we encoded
it, we just simply decode it.
And when you write this
code and you look at it,
the code itself is very simple.
What's interesting
is when you think
about how your application is
structured and see what it is
that you need to
save and restore.
The code is very boilerplate.
We also kept track of whether or
not we were showing those bars.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Again, we just simply
decoded Boolean and set it up.
Now, I'm not showing you
the code that actually goes
and draws the image
and sets the title.
In your application,
you've already written
the code that does that.
When you first presented
that image view controller
or used the segway transition,
you obviously have some code
that goes and loads
in the image.
And with state restoration,
you'll always be
able to reuse that.
So here, all we're trying to do
is get the information we need
and then pass it around.
And I'll show you that in
a little more detail later.
So do we have to
do anything else?
I mean, we had a lot
of different things
getting restored,
and I just showed you
a little bit of code.
Remember, the navigation
controller remembered to stack.
It remembered that we had
this view controller presented
on top -- I'm sorry -- pushed
on top of that collection view.
We also had scroll position
and our selected self.
And we also had that image
sized up as it should have been.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Did you have to write any
code to do any of that?
Well, it turns out you
didn't have to do anything.
You get all of that for free.
And how does that happen?
Well, I mentioned
before that when you set
up all the restoration
identifiers, it both informs us
of what needs to be saved but
also allows us to call methods
to save and restore state.
And we have a whole bunch
of base implementations
that save all the stuff for
you which is very convenient,
saving you the tedium of having
to write these very
common things.
So we keep track of a
lot of default behaviors,
a lot of information
automatically for you.
So you get a lot of leverage,
you get a lot of bang
for your buck for
setting this up.
So let's talk about
what's new in iOS 7.
We had some great
successes this year.
We've seen people
implementing this
and some really nice apps
incorporating state restoration.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
And we felt that we had a nice
starting point for all this,
but that there were
some additional things
that we could do that would
make it even easier to use,
and also we wanted to continue
to grow as we add new facilities
to iOS and as apps continue
to get more sophisticated.
So let's look at what we've got.
We found that with view
controllers and views,
you sometimes wanted to also
have objects participate
in state restoration
like a data source
or some other nonview
controller object
in the same way the
view controllers
and views participate.
So we added the ability for any
object to be able to get added
to the state restoration graph
and save and restore its state.
Also, we've made it
easier for you to figure
out when you can apply state.
Something that's a
little tricky sometimes is
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
when your application restores
all of its state but isn't sure
of the order that different
things will get resorted in.
So after you've pulled the
view controller state back,
is it safe to go
and draw everything
on the screen, or
apply everything?
Well, it might depend
on other objects
that are also participating,
and we don't want you
to have to worry about that.
So I'll show how we've
made that easier to handle.
We've done a lot of work
with Snapshot handling.
Previously when an
application restored state,
we would show the default PNG,
and when the application
started up, we would just jump
to wherever its state was.
Now to provide a more
seamless experience,
we'll use a Snapshot
very aggressively,
and I'll go through
how you do that
and how you still have
some control over that.
There's also some small
enhancements I'll discuss.
We just keep adding new things.
Last year in 2012 when
I talked about this,
I mentioned you can implement
some state restoration
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
for your app and then
enhance it or extend it
as you add new features.
And we're taking our own advice
and doing the same thing,
so we started with the most
critical things, the basics,
and then we started to add
more things for this year.
So that's what's new.
Okay, first is generalized
objects.
So the idea here is to be
able to take objects that are
in view controllers and
views and also use them
in the same way in state
restoration as you've been able
to with our existing API.
So here's two of our
view controllers.
We have both the collection
view and then we also have
that image controller.
And a very common thing
in an application is
to have a data source object.
And with that data source,
you'll be referring to it
for multiple objects
in the system.
So here I'm using the
data source to inform you
of what images I have, how
many, what their titles are,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
and to get the image
bits themselves --
kind of wraps over where
all those images are hiding
in the file system
so I don't have
to replicate that
code everywhere.
So I'm going to want
to be able to save
and restore a reference
to that directly.
I don't want to actually
encode the object completely
and make copies of it; I
just want to refer to it.
And the data source doesn't
actually need to save
and restore any state of its
own, but I will want to be able
to refer to it when I'm saving
state for other objects.
And something that's
kind of similar is
when you have a shared
object amongst the many view
controllers that
might be dynamic.
So I'm going to extend the
demo that I showed you before,
and we're going add an
Inspector so we can set
up some effects,
some image effects.
And here you can see that
that Inspector and the image
which is now kind of
a different color,
are sharing a filter object.
And this Filter object does
have some state, so it will want
to save and restore state,
and I'll also want to be able
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
to maintain those references.
So let's look at how our
example application has changed.
So I've got a data source object
here which I just described,
and it's being referred to by
both of the view controllers.
Now, I'm going to
extend the application
and add an Inspector, so I'm
going to present something
and it's going to
use a filter object.
In fact, I might have more
than one filter object,
and they'll be referred
to for multiple places.
So what I want to focus
on here is the data source
and these filters.
These are these objects
I'm referring
to as generalized
objects, and we're going
to see how they can play
along with everything else.
The first thing you need to do
if you want to use these objects
in state restoration
is register them.
We've added a new
method, on UI application.
It's called RegisterObject
for state restoration.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
I'm glad we have code
Complete, because that's long.
This is very similar to setting
up a restoration identifier
on a view controller.
You pass in the object
and you can see
that it actually implements
a new protocol, and we'll get
to that in a minute,
then you also pass us a
stringRestoration identifier.
Now, for view controllers and
views we made this a property,
but I didn't want to
force developers to have
to subclass a base class
that we would contrive just
so that they could
register an object.
We need to hook in,
we need to know
when the object's registered.
I didn't want you to have to
subclass something and make sure
to call Super, so we felt
this was a nice clear way
to identify what's
participating.
You just call this
method with your object
and the identifier you
want, you're good to go.
So let's look at that
protocol that I mentioned.
It's called UIStateRestoring.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
It has the methods to save and
restore state for that object.
These are actually optional.
There's some cases where you
won't actually need to save
and restore anything; you
just want to add the object
to the graph of objects
we're keeping track of.
We also have a property
that you can use
to scope the object's
identifier.
Sometimes you might want
to use the same object
or the same class of object
in more than one place,
and you'll have creation
code or something that sets
up the identifier, and it would
be contrived to have to give it,
the restoration identifier
some weird name just
because of where you put it.
As an example, let's
say on an iPad,
you have an image editing app
where you can apply
two different effects.
So on the left here, you
can have one set of effects
that you're showing
on the image,
and you might have a
different set on the right side
so the user can kind of
compare and contrast approaches.
And you might be using
the same effect objects,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
or the same filter
objects, and you just want
to give them the same name.
You can scope them by
setting their parent.
Often you'll set that
to a view controller.
You might set it to
another restorable object.
This is what we implicitly do
for the view controller
hierarchy.
We actually keep track of
the parent of an object based
on whether it was
presented, or if it's pushed
on a navigation stack or in
another collection controller
like a tab view, and here
we make it very explicit
for you to do.
Also, in some cases, the objects
may or may not exist depending
on what the user has done.
So I showed you this
beginning of the demo
and I didn't create any
filters, so there aren't any
in my examples so far.
Now, if my application
starts back up, I don't want
to create every possible
object I might have
on the off chance I
might be restoring them.
It'd be a lot nicer if somebody
kept track of that for me
and told the application, "hey,
create this because it existed
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
when you saved state".
So by specifying a restoration
class, you're telling us
who we can ask to recreate
this object as needed.
And in the example
I'm showing you,
all these different pieces here,
these filters are
actually created on demand,
and we'll see how
we can do that.
This protocol that you use
to create the object
is very simple.
So we've specified
a restoration Class.
When we're restoring state
and we need the object,
we call a class method,
objectWithRestorationIdentifier
path,
and it gets passed
in two things.
We give it an array of
identifier components.
The last component in that
array is the identifier,
and often that's all
you'll need to look at.
However, that array contains
the identifiers for all
of the restoration parents.
That way if you've scoped it
and you want to be informed
about which one of these
objects we're asking for,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
you have that information.
We also pass in the
coder that was used
when you initially
saved the object.
So why do we do that?
Why not just let you
create an object.
Well, you're probably
going to go
through some initialization
code,
you may need some resources, and
it might be convenient to look
at a little bit of
the information
that was saved to
help inform that.
Also, let's say that you
have a dynamic database,
and we're restoring an object
that no longer corresponds
to anything in the database.
You might have synchronized
the database or changed things
so that that object
doesn't exist.
You can look in the coder
to find out if indeed
that object is still around
or not, and if it isn't,
you can just return
nill from this method.
We'll be fine; we'll just
stop paying attention to it,
we won't try to restore
its state.
You return one of these
state restoring objects,
and you'll all done.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So now let's look at when
we launched the app before.
I mentioned when
you call application
willFinishLaunchingwithOptions,
or rather, when we call it,
you don't have to do anything
different than you already did
when the app started up.
But remember, in that method,
you're setting up the basis
for your application, and if
we have a data source object
that we want to refer to,
you're probably going to have
to do something to connect that
up when you do your launch code.
And here I'm adding
one line to register it
for state restoration.
Just as I showed you before,
we've given it an identifier,
we've set up our object, and now
we know about it, we can find it
if you refer to it while you're
restoring state, and when you go
to save state, we'll
keep track of it for you.
So let's go back to
that segway code.
So before we even save state,
remember that we want it
to switch to this
image controller.
We're going to also hand
that image controller
to the data source as well;
that way, it knows who to ask
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
to get the actual image
file and to get the title.
So here we go.
Now, we've got all the
information we need
and we've got a nice shared
object that we're using
to keep track of everything.
So what do we do
when we save state?
Well, again, we want to
keep track of that image
and the title, so we're going
to write out an identifier
for those, but we're
going to write
out an additional
thing, and what's that?
Well, we want to keep
track of the data source,
so that when we restoreState, we
can get a pointer back to that
without having to do
an EndRun with globals
or doing something
contrived to try to find it.
We can just refer to it.
How do we do it?
Same way we save anything,
we'll just encode the object.
Now, we're not going
to make a copy of this,
because it's been registered for
state restoration and we know
about it and we know that
it's a state restoring object,
so we'll just save a reference.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
That way you don't end up with
a bunch of disjoined copies,
strewn about your application
that aren't the right thing.
How do we restore it?
Well, you can probably guess
what's going to happen here.
We're going to pull that
data source back in.
That's going to inform
us of what image we want.
Remember, we saved and
restored the image identifier.
And now we're pointing back
at that data source object
so we can use it and
everything's wired up just
as it was when the
application was last running.
So it makes it really simple
to maintain object graphs.
And again, we just use that
coder, simple little line
of code there, decode it,
and you're good to go.
Okay, I'm going to show another
demo that adds on the first
so let's take a quick look.
Okay, so here we are,
back where we were before.
Let's go back to this image,
make it a little bigger.
So let's say that we want to
put a cool sepia effect on it.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So here's that Inspector --
you might recognize it from one
of my screenshots earlier --
and I'm going to dial in
a whole bunch of sepia
because I don't think you
can ever have too much.
See what it looks like.
I'm a big fan of
sepia, actually.
I think it can use
even a little more.
Yeah, I like that,
nice and gold.
And maybe I can soften
it up with some blur --
that's my other filter.
Not so much.
I don't really like that, so
I'm just going to turn it off,
but I'm going to keep
it in my back pocket.
Maybe I can tweak
it a little more,
make it kind of nice again.
All right so let's put the
application in the background,
want to go off and do something
else, there's something exciting
on the Internet, don't
want to miss that.
And then I usually
spend probably too long
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
on the Internet, so the
application is exited now,
so we're going to
start it back up.
And let's hope that
it comes back.
Again, my goal here
is unexcitement.
So there it is.
Now, it remembered that we were
showing this filter inspector.
It also remembered that it
was disabled and that I had
that radius set kind of
low, and it even remembered
that we'd already applied
a filter to that image.
Now, in an image editing
app, you'd probably play
around with filters and
eventually you'd commit them
and then it would save that
information in the database,
but here I'm kind of in
an editing or playful mode
where I haven't really
committed anything;
I haven't modified this
image in the database,
so I want state restoration to
keep track of what I was doing.
So it got everything else,
too, just like it did before.
If we go back there we can see
it's got our collection view
and all that good stuff --
okay, so we got everything back,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
no gasping, no flashing.
Great. All right, what happened?
What did we actually do?
Well, just like before,
we brought back the image
and the image title.
but now we added
this new inspector.
And it was presented, it was
a presented view controller,
and that just kind of came
back in the right place.
Everything was good.
And we also had two filters
and these were those new types
of objects that also get to
play with state restoration,
and they had a little
bit of state,
which they saved and restored.
And then finally, we
actually were able to apply
that to the image, and I did
that kind of behind the scenes.
And this is interesting, because
when you do state restoration
you don't want to just have
one of those Hollywood houses
where it looks like a house
but if you ever walked inside
there's nothing behind it.
You actually want to restore
the entire structure of the app
so that it presents
kind of a continuous
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
and seamless experience
when the user navigates back
to where they were, that they
don't get anything unexpected.
Now, when we initially
presented that inspector --
again, I'm using storyboards and
I did a segway -- what did I do?
I created that filter object
and then when I present it,
it had that filter to refer to.
That way I could
share the filter
between both this
inspector and my image.
You may have multiple objects
that are all sharing some state.
I don't want to get two separate
filters eventually when I want
to restore state, because as
I change the settings here,
I want it to be operating
on the same filter that's
being used by the image.
By the way, again, this
is what's happening
in the application before
I've even saved state.
This is the, how did I
get to where I am piece.
Now we want to save that off
so we can restore ourselves.
So what do we do?
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Again,
we use encodeRestorableState
with Coder.
We're going to want to
save, whether or not
that filter's enabled, and
also what we've got it set to.
But here's the interesting
thing.
Remember that I've got
this filter object.
Why don't we use
that to inform us
when we restore our state
rather than save it separately
in the inspector, so
all the inspector has
to save here is a reference
to this filter object.
And this filter object
itself will implement,
encode and decode, so
it will save its state.
And we had one other object that
also referred to the filter,
and that was the
image view controller.
It'll also save a
reference to the filter,
but they're both pointing
at the same object.
So let's look at restoration.
Yes, the filter object
is shared.
So let's go through it in
a little bit more detail.
So when we wanted to save state,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
we needed to register the filter
objects with state restoration.
I've drawn them in a big
bright blue on the side
so you can't miss them.
We need to save references
to them.
We want to save the information
that those filters have,
so what we need to do is
we need to register them.
Very simple, similar to what
we did with the data source.
We're going to save a reference
from our image view controller
to those filters and also
from the sepia inspector.
And then the filters themselves
will save their own info.
That's just whether they're
enabled, what the intensity is.
Now, we're going to do
everything kind of in reverse
when we restore, so
it's very symmetrical.
But there's one other
thing we need to do
when we're restoring state.
Remember that these filters
may not have existed;
if I hadn't used them,
they wouldn't be there,
so I wouldn't need them.
So we're going to consult
the restoration class
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
to restore them only
in the cases
where the user was actually
using them, when they existed
at the time we saved state.
So that's what we'll do here.
So we'll recreate them,
we'll then restore
references, here's our filters.
So we've just created
them again.
The state restoration runtime
will ask the restoration class
to create them by calling
objectWithRestoration
identifier path.
Now, you'll notice they
don't have any state yet,
so what's going to happen next?
Well, we're going to restore
our references to them,
but they still don't
have any state.
We're just going to wire them
up to our view controllers,
so we can see there I
haven't actually applied
that filter yet.
So we'll let the filter
object restore its own state
recalling decode.
So now it's restored its state.
I don't want you
to have to worry
about what order this all
happens in, so I'm going to talk
about how we can leverage this
later and make it real simple
to apply it so that
you then eventually get
that nice sepia look
on your picture,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
and we're back to where we were.
So let's talk about
how we apply the state.
The first thing we do as we're
restoring is get back our image.
But we can't go and
apply that filter yet.
The filter might not
even exist at this point.
We're just in the decode method
for this view controller object.
And at some point we are
going to go grab that filter.
It'll get created, it
will restore it state.
Now we're good to go.
Now we can apply that filter.
But when? How do we know?
So we've added a new method.
This method's for view
controllers as well as any
of the state restoring objects,
and it decouples decoding
your state from trying
to apply what might be
more of a global state
across the whole application.
It's just a simple
little method,
application finished
restoring state.
It's kind of like awakeFromNib.
It's telling you, everything
is restored at state;
everything is created
that we know about;
it's safe to go in the water.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Go on, apply your filter.
So if we didn't have this, you'd
probably have code like this.
Now, I mentioned I wasn't
going to show you much code;
this is actually the
only code slide --
so mainly because I
like this cool effect.
So the idea here is I don't
want to update the image
when I'm decoding my state
because everything else might
not exist yet, so I'm just going
to decode my state
and not do that thing.
And then when application
FinishedResotringState is
called, I'll call updateImage.
And if you're wondering,
what's this updateImage,
imagine I don't have
any stateRestoration.
I have a segway to
a view controller,
I tell it what image
I want it to show,
there might be some
filters that are set up.
So I'm going to write some
code that goes and fetches
that imageAppliestoFilter,
sets the title, do all of that.
That's updateImage,
and if you look
at the sample code you'll see
that I'm able to just reuse
that method when the application
restores its state here,
so I don't have to go and write
a whole big bifurcated strategy
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
for restoring the image and
setting it up in this case
versus that case; got one
cute little piece of code.
So there is one area where
we got a number of questions
and they were kind
of the same question.
When do I have to
create my objects,
and when will you
automatically find the ones I've
already created?
You have a lot of
view controllers.
You have some of these new
state resorting objects.
And some of them exist when
the application starts up;
others come into
being dynamically.
So let's look at that
in a little more detail.
So if you think of the set of
application objects you have,
I like to refer to
everything that exists
when the application is
already started as base objects.
These are loaded from a
storyboard or from various nibs.
They're created by your code
in application
WillFinishLaunchingwithOptions.
Before we do state restoration,
they're already in
play, they exist.
So if they're going to be using
state restoration and we need
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
to find them, you don't
have to do anything.
We'll just be able to
find them and look them
up because we've got some
magical little tables
where we keep track
of everything.
What about all the
dynamic objects?
These are presented controllers,
view controllers you might have
pushed on a navigation stack,
some of these restorable objects
like those filters --
how do you get them?
Well, if it's a view controller
and it's in a storyboard,
you actually can get
it completely for free.
We'll just find it.
That's because we know what
storyboard it came from,
we know what its
storyboard identifier was,
so we can keep track of it.
So you'll notice
when I presented some
of those view controllers there,
I didn't show any code to have
to recreate them and represent
them; we were able to go
and hoist them out of
storyboards ourselves.
However, if it's
not in a storyboard,
or if you want a
little bit more control,
you have to write a
little bit of code,
and that's where the
restoration class comes in.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So you can use a restoration
class for view controllers
in the same way I showed you for
these new generalized objects.
And also for view
controllers, as a fallback,
we'll ask the application
delegate
for them if all else fails.
Now, if you implement
a restoration class,
that trumps everything.
That gives you control.
Let's say -- going back
to my earlier example --
maybe you have a dynamic
database with images.
These can synchronize and
images come, images go.
So if the image that was being
shown when you saved state isn't
in the database when you
restore it, you may not want
to bring back that
view controller
and show us something
ugly like imageNotFound.
You want to just
avoid it altogether.
And it's always fine to
return nill when we ask
for a view controller.
We'll do the right thing.
If it was presented, we
just won't present it;
if it was pushed on
the navigation stack,
we'll go one back and
so on until we get
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
to a view controller
that still exists.
So we give you that control
to make sure that we don't go
and force you into
some boundary situation
where you don't have all the
assets you need that make sense.
So here's the base objects in
the example app I showed you:
the data source, that
navigation controller,
and the collection view.
They were all in
my main storyboard;
they just got created
by the time I got
into application
WillFinishLaunchingwithOptions.
And I put that Inspector in a
storyboard, so then I was able
to just completely
forget about it.
I didn't have to recreate it;
it just magically came
back, it was presented.
Now, if I want a
little more control,
I can take that image
controller, and even if it's
in a storyboard I can still
give it a restoration class,
or if I was creating it out
of whole cloth, myself encode,
I'd give it a restoration class.
And the filters, as we saw,
had a restoration class
which recreated them on demand.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So when you think
about your objects
and which ones need
a restoration class,
which ones you'll
have to recreate
when we're restoring state,
break them up in your
mind, what's a base object.
Then you don't have
to do anything,
and of the dynamic objects,
which ones do I actually
need control of
or what am I going
to have to create?
Makes it a lot simpler.
I mentioned we did a lot
with Snapshot handling.
So in iOS 6, we wouldn't
use the Snapshot at all.
And that was kind of jarring,
because the application would
come up with the default PNG,
restore its state and then kind
of slap you when it went back
to where it actually was.
So we're trying to be
much more aggressive now.
And we'll show the
Snapshot wherever we can.
So when we launch
an application,
if the Snapshot's available
and if we can use
it, we'll show that.
It looks much more seamless.
However, there might be cases
where you want to use it,
so we give you some control.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
We've added a new API.
This is on UIApplication
and you can tell it
to ignore Snapshot
on the next launch.
You'll do this while
we're saving your state.
So if we're saving state in
one of your view controllers,
notices, this is kind of tricky.
I might not come back exactly
to the same place,
ignore the Snapshot.
Then we'll use the default PNG.
That means only one of two
things will ever happen.
We'll show the same Snapshot
that we're going to launch to,
or we'll show the default
PNG, which is predictable
and at least it's somewhat
familiar, second best choice.
So why would you want us to
ignore the Snapshot, though?
Well, here's some examples.
Maybe your application is
showing a network error.
Can't get on network;
please try again later.
Click the application in
the background, it exits,
user goes to maybe a better
coffee shop with free Wi-Fi,
they start the app up again.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
And it would be kind of
silly for this network thing
to be showing, seeing
that there's an error
freaking the poor user out,
and then have it
just dismiss itself
or disappear when
the app started.
You also might have things
like a table view of data
that changes frequently.
Maybe you have the top
10 awesome kitten videos
of the day, something
like that, and they tend
to change pretty quickly, I
can tell you from experience.
So the application when it
starts up, you might not want
to show a list of things
that you're actually not --
eventually going to land on,
and the user's stabbing away
at this video and it doesn't
show up, so it might be better
to avoid that altogether.
There's also some implicit cases
where we'll actually detect
Snapshot wouldn't be the best
choice, so there's
a couple there.
One is, if the top most view
controller doesn't have a
restoration identifier,
we're going to assume, okay,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
you're app's pretty awesome
and it restores state,
which is great.
Thank you, but we can
tell that you're not going
to restore to this one.
There might be some
boundary area of the app
that users rarely go
to, setting something
up that they would
typically only do once.
That might not be a battle worth
fighting for the first release,
so you may not save the state of
that particular view controller
and you may be focusing
on more important things,
such as articles they're
reading and things like that.
So in that case, if we
detect there's no restoration
identifier, we won't show
the Snapshot; we don't want
to give the user false hope
when it's starting back up.
Also, there may be some
things that we don't restore,
and I'll show you
an example of that.
So there's a whole
bunch of areas
where it's either inefficient
to try to restore everything
or it's just complicated enough
that we haven't gotten
to it yet.
Similar to the advice
I gave last year
where you take an
approach where you start
with the most important
things and then add to it,
we're in the same boat.
So we don't want
to show a Snapshot
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
if we know we're not going
to come exactly back
to the same place.
So I'm going to do another demo,
but this one's kind of quick,
so I'm actually going
to use a movie.
So here's our application,
we're looking at the image,
and let's say that I go
to the activity sheet.
Okay, so we actually don't share
the activity view controller
state, and there's a couple
of reasons why we don't.
Mostly, it's because the
application is handing us a
whole bunch of data.
It could have handed us a
video to mail to somebody
to really annoy them, it could
have handed us a lot of content,
and it can often be a
[inaudible] content or something
that may not exist when the
application starts back up.
So it would be both
be very costly for us
to save all this data
and it might also bite us
when we start the app back
up but we can't find it.
And it could be confusing
for the user
if they start the
application back up
and they just see this
activity view controller
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
and they're not really sure what
it was they were trying to share
or what they were trying to do.
And you can see the
image peaking
through a little bit here,
but in other cases you
might not see that.
So what do we do if we put the
application in the background
and it exits and we
start it back up?
So let's take a look.
So I did something
kind of subtle here.
It might not have jumped out
at you, so I'm going to replay
that kind of in slow motion.
So let's see the
application start back up,
now watch the transition.
See how it kind of faded in?
It's very subtle, but let's
look at it if it doesn't do it.
Starts up, boom.
Kind of jarring,
kind of disjoint.
See it again.
So that's kind of unpleasant
as a user, and let's look
at what we do regular speed.
Nice. It just kind
of fades right there.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So at least we're not
slapping the user's attention
around with this.
So that shows a few
different things.
Already in the previous
demos that I did,
you saw us using the Snapshot,
so that covers the first part.
Now when the application
starts up in restoreState,
it really does look seamless.
You might have even
thought I'm cheating
and I didn't really kill the
application but I promise I did.
And in this example,
we're showing how you
both have control,
how we'll sometimes
implicitly notice
that we shouldn't use the
Snapshot, and how we'll do kind
of a nice transition
in these cases to sort
of ease the user back in.
So we've added a
few enhancements.
I'm just going to go over
them kind of briefly now.
We've got some keys that are
available in every coder.
So in the initial
implementation when we came
out with this last year,
an application could look
at the bundled version that
was used when it saved state.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So it could tell,
this old version
of the app saveState
I've changed drastically
so as I restore the
state I might have
to do some transformations,
or maybe I don't actually want
to restore state from this
very, very old version
of the app, it's just too much.
And that information
was available
in every one of the coders.
Most of your app might be
fine with this old state
but there might be one view
controller that's changed more
radically, so it
would look at this
and say, "hey, wait a minute.
Let me see who saved this state,
our old version of the app."
We've added a few new keys that
accomplish very similar things.
The first one is to tell
you the system version.
You may have noticed iOS 7
changed a little bit from iOS 6,
so your app might
need to adjust metrics
or do something different
when it's restoring state,
so you can tell what version
it ran on when it saved state.
Also, we put the time stamp
from when we saved state.
This was a very common
request, as it's so useful.
In some cases, after an app's
been inactive for awhile,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
you might actually want
to restore something a
little bit different.
You might want to go
back up to the beginning.
Because it might be confusing
to the user if they dig way,
way down in the app
and then a month later,
when they run it again,
it goes to something
that doesn't even make
any sense anymore,
something that's
no longer relevant.
So you'll want to take
maybe a little bit of notice
of what time or the date
that this was saved.
It made it easier to
handle static table views
where their content
doesn't change.
Now, because a table view
can have dynamic content,
in the first release of
this, when we saved state
and we wanted to keep track of
selected cells and the top cell
that you were scrolled to,
we would actually ask the data
source for the table view,
what is an identifier for this
cell that makes sense to you?
The reason we did that
was because the cell
at index 3 today may not be
the cell at index 3 tomorrow.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
It may not even be the cell at
index 3 in 5 minutes or an hour.
So we don't want to just
select something based
on its index path if
that data could change.
So we would ask the data source,
who knows all about the data,
to give us some identifier,
which we would save instead
of just the index path.
And when we restored state,
we would then do the converse
and say, "here's an identifier;
where's the index path now?"
And that works out great,
especially if you have something
like music or photos or a set
of articles that could be moving
around and getting
sorted and changed
with things getting
deleted and added.
However, it's also pretty
common to use table view
for a static set of things,
maybe some switches
that you have.
Maybe that inspector
I have there might end
up with a fancy table view
with a whole bunch
of different items.
And it's kind of a
lot of work to have
to convert these index
paths into some identifier
that you contrive
and convert it back
when they're never
going to move.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So now, if your data source
doesn't implement the protocol
we detect to ask this
question, we'll just go
and save the index
path directly,
and it can be really useful.
And speaking of selected and
scrolled, we now added support
to UICollectionView.
We'll remember all the selected
cells in a collection view
and also of the scroll position.
And you actually saw
that in the demo,
so we do that all for free.
And then finally, one thing that
can be extremely frustrating
to a user is when
they're composing mail.
Let's say that you find
a really awesome image
or an interesting article
and you want to share it,
so you bring up the activity
sheet as I showed before,
and continue all
the way to mail.
So now you're not
in the activity view
controller anymore.
That's gone, you're now actually
in a mail composition
controller.
And you type this
elaborate message,
put the app in the background
for whatever reason, it exits,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
start it back up -- oh, no.
Gone. So we thought that
was a really important thing
to keep track of for users as
mail can be fairly lengthy.
So if you have an
activity view controller
and you give it a restoration
identifier, we'll keep track
of mail drafts and
we'll save them
and we'll bring the user
right back and restore them.
And over time we plan to
add more to this as well
so that we'll start saving more
and more specific activities,
but we thought mail was
the best place to start.
So I just want to take a moment
to talk about security and how
that relates to state
restoration.
We actually use data protection
on the archive itself.
So the state restoration archive
that's saved is not accessible
when the device is locked.
It's an encrypted file, and
if the device is locked,
even if somebody stole your
phone, managed to break
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
into the file system
and pulled that off,
they wouldn't be
able to look at it.
So why do we do that?
Why is it important
to protect this file?
Well, there's a few reasons.
One is it's really easy to
leak sensitive information
by mistake.
You might keep track
of the scroll position,
which could inform somebody
who really knows what
the app's all about,
how many entries you have
and how you've been using it.
There might be identifiers
that you use for some
of your view controllers
or some of these objects
that actually give away a
little bit of information
about what a user is
doing with the app,
and for some business apps
or sensitive personal apps,
you might not want
anybody to know about this.
You might be adding
a new contact
and it's not saved
completely, but you're adding
in someone's personal
information;
you want that to
be kept private.
Also, your application already
may be keeping things private.
You may already be using data
protection for the application,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
and we don't want to be the
weak link in that chain,
so we felt that it was
very important to make sure
that the state restoration
archive was protected
and that you didn't
have to worry about it.
So the action on your part,
you don't have to do anything.
You can put whatever you
want in there and not worry
about anything leaking out
because we're protecting it.
I'm going to come back to
that in a minute and see how
that relates to this topic.
So we added something
really neat on iOS 7
that allows applications
to run in the background,
and they can go and fetch assets
or do calculations or get ready
for when the user's going
to use the application,
which is pretty cool.
So if we're doing some
background operation
and the application
does state restoration,
we want to make sure
that everything comes
back predictably.
So what do we do?
So this is my application,
and let's say
that your application just gets
started up in the background
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
and here's my little magic wand.
So your application just
ran unbeknownst to the user,
did its thing, and since
it participates the state
restoration and since the device
unlocked, when it starts up,
we go through the standard
state restoration flow just
like we normally would.
That's because the user might
take their unlocked device
and switch over to your
application, and we'd want it
to a restored state and be back
where the user expects it to.
But what do we do if
the device is locked?
Well, we can't access that
state restoration archive,
so in this case we
take a simpler path.
We let the application get its
work done, fetch its assets,
do whatever it needs to do in
the background, but we don't try
to restore state, we won't
be able to access the file
and it will fail,
and very importantly,
when the application finishes
running in the background,
we won't clobber the state
by saving whatever
its current state is.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
If the application is running
while the device is unlocked,
if it runs in the background,
when it completes its
background task and notifies us,
we'll save state again.
The application may have
changed its appearance,
it may have changed some of
the behavior when it did this
in the background, but
if the device is locked
and it couldn't restore state in
the first place, we're not going
to have it save state; in fact,
the app's just going to exit
after it's all done
with its work,
so that it's just getting work
done, loading those assets,
but not perturbing the user,
and when they start it back
up it'll go back to where
it was last time the device
was unlocked.
One other thing about this,
now that it's more likely
or that there's more potential
for our application to run
in the background, if you're
restoring state, keep in mind
when you start up,
the application's
data may be different
and that you might want
to take that into account.
And state restoration
is built around the idea
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
that you should be able to save
state and then restore it even
if all your data has changed.
Many of the things I was
describing before are
about that very thing.
But just be sensitive
and keep it in mind.
So now you've gone and
implemented state restoration,
but you want to be sure
that your application's
doing the right thing.
So do we have any tools
that can help you?
Well, one of the problems
with developing this
that you might run into, the
archive itself is fairly opaque.
It's not a text file.
You can't just go and look at.
So how do you tell
what's in there?
How can you ensure that you've
actually saved what you intended
to save and make sure
that we've kept track
of everything that you expect?
Well, we have a new tool that
we're going to make available
from our developer support.
You can run this tool and
it'll output everything that's
in the archive.
I'm just going to show
you a little bit of that.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Kind of looks like one of those
1960s movies about computers
so very textual but
very informative.
So you're going to see a whole
bunch of stuff like this,
and this is just kind
of the top of it.
So this first tells you what
objects have restoration
classes, and that's real useful
because a common problem is
when you forget to set a
restoration class or you set one
by mistake on something
that doesn't need it
and you're wondering,
either you're not
getting any objects back
or you're getting two
for the price of one.
So right here you can look and
see everything that we expect
to use a restoration class.
Also, we just have a set
of top level information.
So I mentioned we keep
track of the bundle version.
We also have these new keys,
when did you save your state.
What was the system
version that was used
when we saved the state?
And then for all of the
objects that are participating,
we save a bunch of
information about those.
So here's our image
view controller.
So you can see on the top
there highlighted in yellow,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
we tell you the type of object
it is, so you can kind of scan
down -- very quickly
looking for view controller,
view controller -- and it also
tells you the class of it,
so you can look at
it and go, "hm,
that's not the class I expected.
How did that happen?"
And you can also look
at what it saved.
And this stuff is very,
very straightforward, right?
We referred to our data source.
Notice it says, "object
identifier proxy."
That means this is a reference
to an object that's
also participating
in state restoration and
not an actual object.
Here, I actually put my
filters into a dictionary,
so this is just a textual
representation of a dictionary.
So the dictionary
itself was encoded
and the keys were encoded,
but you'll notice again,
we're using proxies
for the actual objects,
the filter objects
in the dictionary.
And here's the identifier
for the image as well.
So sometimes when you're
trying to diagnose issues,
you can learn quite a lot simply
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
by taking a look
at your archive.
And I think it's very useful
as you're building new code
and you're testing things,
to just take the time to go
through here and make sure
everything you expect is in it.
Now, I mentioned that
we can pull things
out of the storyboard
automatically.
So here I'm just showing that
we keep track of the name
of the storyboard and also what
the storyboard identifier was.
And that might be
useful to you as well,
even if you're not
depending on this behavior,
just so that it helps
you to triangulate
on what this object actually is.
You're not going to have a
picture of the view controller
on the right as I'm showing
here; you're just going
to be looking at text
and you might think,
what is this thing
I'm looking at?
Oh, came out of the storyboard.
There we go.
Okay. How about these
generalized objects?
Again, we do the same thing.
We're showing that it's
just a general object,
it says restorable object
-- that's my terminology.
We show the class of it, and
it saved a couple things,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
so we just keep track
of all of that.
So pretty dry, but I think
it's very interesting
when you see all of this.
Okay, I'm kind of
running out of time,
but I have a couple more things.
We've added a profile
you can install
and we'll log a little
bit of extra information.
You can also set a
default for the simulator.
So you get kind of a play
by play in Xcode's console
as your app is saving
state and restoring it,
and it could be pretty
useful to look at that, too.
And we have a profile that
you can install that puts you
in what I call developer mode.
That's what I was
using in my demo
so that I could kill
the application
without losing state
restoration.
And it's really useful
when you're chasing a bug
or you just want to check things
and you want to be able to go
on the switcher and kill it but
not actually lose the archive.
So we did a recap.
I'm hoping that if you
haven't used state restoration,
this shows you just how easy
it can be to incorporate it
in your app, and you'll go back
and you'll want to take a look
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
at this and see what you can do.
And for those of you who
have already been using it
and new people as well, I hope
that the new features show you
that you can also extend things
and it makes it even
easier to use.
Talked a little bit
about security
and background operation.
We covered the tools that
we have, and that's it.
Jake Behrens is our
Frameworks Evangelist.
He'd love to hear from you.
We've got some great
documentation online.
Of course, the forms are always
available, so talk to him
if you have a chance,
and that's it.
Thank you very much.
[ Applause ]