Transcript
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
[ Silence ]
>> Chris Dreessen:
Good afternoon.
[ Applause ]
Welcome to session 213.
This is best practices
for Cocoa Animation.
I'm Chris Dreessen.
I'm an AppKit Engineer
so I work on this stuff.
We have a lot of great things
we'd like to show you here.
So I'm going to give you
a basic overview of this.
So I said a lot of great things,
I meant a lot of great things.
I'm going to start with a
brief overview of how Animation
in Cocoa works, how to use it.
We're going to continue
to describe how
to make your own view
subclasses animatable
for your own properties
and describe how to augment
that with context
sensitive animations.
I'm going to share
with you a technique I
like to call chaining animations
where we use the completion
of one animation to start
off another animation.
Some of you are familiar
with the implicit
animation functionality
of NSAnimation Contest
we added in 10.8
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
and I'm going to cover that.
And then I'm going to cover
a bit of Core Animation,
not how you can use
Core Animation
but rather how NSView
uses Core Animation
to do animations
for layer back use.
At that point I'm going
to invite my colleague Peter
Amman onstage and he's going
to talk about this great new
class called NSStackView.
He's going to describe how
you can use auto layout
to animate view positions,
how you can animate those
constraints directly
and additionally best practices
for animating window
size changes.
So throughout the course of
this presentation I'm going
to be talking a lot about
the NSAnimatableProperty
ContainerProtocol and that lives
in the NSAnimimation.h header.
And it's a simple protocol, it's
just five methods but I'm going
to be coming back here a lot
and we'll touch each
of these one by one.
So basic animations, most of
you are familiar with this
but this is how you animate
basic things for NSView.
So back to NSAnimatableProperty
Container,
I said we'd be coming
back here a lot.
I didn't lie.
The method I want to introduce
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
to you here is the animator
method which returns
to you the thing we
call the animator proxy.
And this is an object that
implements all the same methods
that your object does.
The difference is when you
message it it will animate them
instead of just popping
to the final location.
So suppose you want to
fade the Alpha of a view.
You want this to disappear?
Well that's not too bad,
that's just one line of code.
We set the Alpha value to zero
through the animator proxy,
view.animator.alphavalue=0
and that's all it takes.
And here's something
more complicated.
This is, we're sliding, that's
got to be way more code, right?
Same thing,
view.animator.frameorigin,
just one line of code again.
Okay here's one more,
we resized it.
That's two dimensions.
That's got to be
four times the work
and it is four times the work.
It's also one line of code.
[laughter] Alright so you're
all familiar with that.
I want to talk about how to make
your own views animatable now.
So suppose you want to do
an animation like this.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
We have a framed view which is
a single red line on the outside
and we change the line
thickness, we grow it.
So your view might
look like this.
You're going to declare a
line thickness property,
it's a float.
And you're going to
go in and drawRect
and this is a simple drawRect.
We just set the red color and
then we frame our own bounds
with a rectangle of the
appropriate line thickness.
And additionally in our accessor
for setLineThickness we
call setNeedsDisplay yes,
after we've updated our I-bar.
So I told you, you know, you
can just use the animator proxy,
that works fantastically.
And if we try this we
see this animation.
And technically that
was an animation.
Things changed on screen.
But I don't think any of
us are really satisfied
with that animation.
So what was missing?
Well back to the NS Animatable
Property Container Protocol.
There is this method called
defaultAnimationForKey.
It's a class method.
And that is our missing
piece here.
And to make this animatable
we're going to go ahead
and implement the
defaultAnimationForKey method.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So our implementation we start
by checking what key we're
actually talking about here.
In this case it's our
line thickness property
which is great.
And we return the default
CABasicAnimationObject
and CABasicAnimation is one
of the animation types
we use most frequently.
It just has very simple
from and to values along
with some properties
that control how those
values are interpolated.
And of course if it's not the
line thickness property we want
to fall through as a super
so the other view properties
can continue being animatable.
And just by adding that if we
do view.animator.linethickness
again we get this animation
which is just what we wanted.
So that's still pretty simple
so let's complicate
things a little bit.
Sometimes you need to
know more information
that you can provide
in a class method.
You need to know what
specifically is being animated.
And you can do that too with MS
Animatable Property Container.
There is a method
called setAnimations
which takes a dictionary,
the keys in the dictionary
are the properties
that are being changed
and the values
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
in the dictionary are
CAAnimationObjects describing
how you want us to change them.
So this is our method here
and suppose we wanted to snap
to multiples of 10
for our line thickness
as we did our animation.
We can't really do this here.
There's no way of specifying.
We don't know what the from
and to values are inside
of the class
defaultAnimationForKeyMethod.
So the way we fix
that is we're going
to use this thing called
CAKeyframeAnimation
and the key frame animation is
another animation type we use
a lot.
And in it you can, well
basically you specify values
to the key frames and you
can interpolate between those
or just have it use
those discreet values
at different time stamps.
And in this case we're going to
populate it with steps from 0
to 40 moving by 10 each time.
So once we create our animation
we turn it into a dictionary
and we set it on the
view.animationsproperty
and that's our new objective
C dictionary syntax there.
We specify the key
as line fitness.
The value is our
key frame animation.
And then we just talk to the
animator like we always did.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
View.animator.linefitness=40
and we wind up with an animation
that looks like this instead,
which is exactly what we wanted.
So that's kind of cool.
But there's a few
other things you can do
with key frame animations
that are neat.
And I mentioned you could, you
know you have your key points
in there that have
specific values.
You don't need to interpolate
between those values.
And in fact with some types
of values you can interpolate.
So in this example I'm setting
these values to an array
of images of various Apple
products and the same as before,
I set the animations dictionary
there using our new animation
for the image property, in
this case this could be the an
NSImageView which
implements an image property.
And I just tell the animator
hey, change the image
to this final Apple TV image.
And something I want
to point out,
my key frame animation
specified five different types
of Apple products but it
didn't specify Apple TV.
And the reason for this is
that the value you tell your
animator proxy is the value
that's going to wind up
in the object ultimately.
The stuff in the animation
is just used along the way.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So if we see what this
looks like, recycling nicely
through the Apple
products in our image well,
and we end at Apple TV.
And you can do this
with other things.
You don't need to do
it with just images.
You can do it with string
values, for example.
In this case we have
a text field.
Our values are various
city names.
And again, if we set the
animations dictionary
on our text field using string
value this time as the key
and our key frame
animation again as the value,
and finally we tell
the animator,
hey your string value is San
Francisco because we want
to wind up in San
Francisco and all
of you here have already
completed the step, good job.
So we see what that looks like.
It animates our text field
just like we expected.
But you can do a
few more things.
You don't need to
restrict yourself
to key frame animations.
So if you're familiar
with NSCell and NSControl,
especially NSTextField,
you're familiar
with these things
we call formatters.
And formatters are objects which
take an arbitrary object type
and return a human
readable string value.
And in this case I'm going
to implement my own
formatter class,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
in this case the cleverly named
my formatter but we're going
to delegate most of our
actual formatting work
to NSDateFormatter.
So our string for object
value method looks like this,
we take in an NSNumber and we
just ask for its double value.
And one of the ways we
represent dates on our system is
as the number of seconds
since January 1, 2001,
that's the reference dater,
in this case the time interval
since the reference date.
And we can construct a new date
from that double value
representing the number
of seconds.
And then we just plug
this into our NSFormatter
and it gives us a nice human
readable string for the date.
And we couple that
with this code.
We go ahead, and I want this to
be a long, slow animation for us
to savor so it could
take 20 seconds.
And here I'm replacing
the animations dictionary
on the text field again,
this time for the
double value property.
Double value is a property most
of our controls themselves
implement,
and again it's a double so
it's easily interpolatable
using CABasicAnimation.
I set the text field's double
value to the initial value of 0
and then I ramp it
up to 1,000,000.
And when you do this
you get this animation.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
We are actually formatting
the interpolated values
for every second between January
1, 2001 and wherever this ends.
So you can also use
this for other things,
for example you can implement
your own double property
that we can interpolate because
it's a double and say ramp it
between 0 and 1 and then
use that as the input
to your own calculation to do a
more complicated interpolation.
And yes, I love you
so much I dug
up the old Chicago font
for that animation.
[applause] So we're going to get
into something I call chaining
animations now which is simple
but can look complicated.
Don't be daunted by this
next slide, that one.
You can be daunted by
this slide, that's okay.
So in this case we just
have a few animation groups.
So we have our first
animation group
and we're setting
properties for our first view.
And then the completion handler
of that animation group
contains another animation group
and we set properties
for our second view,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
likewise the completion handler
for that group contains
another animation group
and we're just setting
properties for view three here
so you can see here's the 3rd
animation group encapsulated
within the second
animation group, encapsulated
within the 1st animation group.
So I'm going to go ahead
and show you a demo
of how you can use
this in your own code.
So let me show you what
this program is here.
This is just a simple
list of fonts on the left
and a rotated shadowed preview
of that font on the right
and we can actually select
multiple ones of these
and we get this sort
of page fanning effect.
And if you've used mail and you
select multiple messages you
notice they do a
very similar effect.
One of the differences though is
that mail actually slides these
messages in from left to right
and then right to left
as they come in and out.
And additionally it implements
this really cool effect
where they slide
in one at a time.
So I'm going to show you how
we can take this program here
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
and implement all of that.
So the important methods to be
aware of are these three here,
update visible views,
our controller object here
has two NSIndexSet I-bars
and an index set is
basically just an array
of specific indexes but you
can perform set operations
like union, interception,
subtraction on them.
And one of these index sets
stores the rows or fonts
that we want to be visible
and another stores the ones
that actually are visible.
So to calculate the ones we
want to remove we take the ones
that are actually visible and
we subtract the ones we want
to be visible and the remainder
there is what we need to take
out of the view hierarchy.
Similarly when we want to bring
stuff in we take what we want
to be visible and we subtract
what's actually visible
and that tells us
what we need to add.
So then we just enumerate
over these index sets.
And we hide the rows
that shouldn't be there
and we add the rows
that are there.
And if we go ahead and
look at the implementations
of these methods,
here's the hide method,
we just have an early out in
case it's already visible.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
We update our I-bar.
We grab a cache view for the
appropriate font and we take it
out of our view hierarchy.
The show method is a little more
tricky but it's not that bad,
same thing, early out, update
our I-bar, grab the view
and here we position it within
our right side container
and we just center it.
So what we want to do
for animation though is
complicate this up a bit.
We want it to slide
in left to right.
So just setting the final
bounds isn't enough.
We actually need
two new variables.
We need one to specify the
origin of view of the view
when it's on screen and
that's actually the same
as the value we've
already computed
so we can take what we
set the view frame origin
and we just assign
it to this sort
of temporary in origin value.
And then we need another one
which says what the origin
of the view should be when
it's off screen or our
out origin in this case.
And when it's off screen we
want the right edge of our view
to match the left edge of the
container view so we're going
to take the MidX of the
container view instead
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
and instead of subtracting
1/2 of our width we're going
to subtract our whole width
and that aligns our
edges like we'd expect.
We don't need this bit anymore.
So one of the things we can do
now though is we're going to,
this will look familiar
if you're paying attention
to the slides, we're going
to make a CABasicAnimation
and we're going to set
its from and to values.
We're going to start at the
out origin and we're going
to animate to the in origin.
And it's important
to remember our steps
of setting the animations
dictionary on the view
so view.animations=frameorigin,
with our animation there.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
And then we tell
the animator again,
hey we want to animate in.
And we actually want to add
our subview before we do that.
So similarly for
removing this we're going
to do a very similar
thing, we're also going
to specify an animation
for how to remove it.
So we actually copy and
paste a whole lot of this.
So we copy and paste our
container bounds, our view frame
and our from and to values.
But in this case we need
our from and to values
to be switched so
that's an easy fix.
And additionally our use of the
animator is also very similar
so we can take that
again as well.
Importantly we're replacing
animating to the in origin
with animating to the out origin
but we have one remaining
problem here.
You'll notice before we
remove from the super view
if we did this immediately we
would tell our view to animate
and before it ever had a chance
to draw we would remove it.
So we're going to go ahead
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
and open animation group
using NSAnimationContext run
animation group.
We're going to take our changes
that kick off the animation
and move them into
the changes block.
We're going to take
the completion handler
and we're going to take our
removeFromSuperview from this.
And this prevents our view
from being prematurely removed
before animations had a chance
to run.
So if we go ahead and run
this and we cross our fingers
that it compiles, yay,
we get this instead.
And you can see these things
nicely exchanging locations come
in from the left and
exiting to the left.
And that works, we
bring multiple things,
or bring them out,
so that's great.
But one of the differences
here is these all fly
in at the same time.
We want them to come in one at
a time so how can we do that?
Well if we go back to our update
visible rows method I'm going
to go ahead and declare some
block scope storage here
and just keep an integer that
keeps track of the number
of animations we've
added in this pass.
So I'm going to begin an
animation group again.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
And I'm going to copy and paste
our code that enumerates our
in and out index sets.
And one of the differences
though is I only want
to remove or add a single view.
So I'm going to go ahead
and enter increment my counter
inside of my enumeration
and the enumeration method
has this handy stop argument
that I can set to yes to exit
early and I'm going to set
that to yes if our count has
been something other than 0,
which it will always
be in this case.
I'm going to make the same
change for adding a view
but instead of always
enumerating the views
to add I'm only going to do it
if we haven't removed anything
so we're only going to get
one removal or one addition
for pass through this.
Now if we didn't add
anything this time, excuse me,
if we did add something
or remove something
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
after our animation has
completed, we want to go ahead
and just run this another time
so we call self-update
visible views
and of course we add a semicolon
here to appease the gods.
So we still have
our nice animation.
But you'll notice as I bring
these things in they come in one
at a time just like they do
in mail and they exit one
at a time just like
they do in mail
and that's exactly
what we're looking for.
It wasn't as complicated
as having 10 animation groups
stuck inside each other.
It was just the act of
invoking our single method
from the completion handler
that we had already kicked off.
So that's how you can
use animation chaining
to implement some fairly
entertaining animations.
That concludes our demo
for chaining animations.
So on to the next thing,
implicit animation.
This is something we
added in Mac OS 10.8.
And some of you have probably
already used it and enjoyed it
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
and or have been
terrified have it.
So before we added this you
would use view.animator.frame
and you would just set
it to a new rectangle
and with implicit animation
you could use this instead.
You tell the current animation
context that it's okay
to allow implicit animation
and then you set the frame
on the view directly.
So you're actually
animating as a side effect
of setting the properties
on your view.
So consider this method here
called swapSubviewFrames.
In this we just take
our first two subviews,
we grab their frame rectangles
and we set subview 0's frame
to subviews 1 frame
and vice versa.
And if we just execute
this we get this,
they just swap positions,
which is correct
but it's not necessarily
what we want.
You can do this with allowances
in animation really easily,
we just setanimation context.
currentcontext.
allows implicitaniimaton=guest
and we execute our
same method as before.
And in this case
it looks like this.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So when would you use this?
You would use this in places
where the animator proxy
doesn't have visibility
into what's being animated.
You'll notice swap subview
frames was an action.
It didn't take an argument.
The animator can't actually
interpolate anything in there
so you can use the
animation context
AllowsImplicitAnimationProperty
to animate those frames
as a side effect of
your action method.
There are some caveats.
First it doesn't
work for everything.
It will work reliably for frame,
frame size and frame origin,
pass that you're
beginning to pray.
But it does tend to
work for more things
when you're layer backed and
we're talk a little bit more
about layer backing in a bit.
In fact we'll talk a bit more
about layer backing right now.
So I'm going to give a brief
overview of core animation,
a very brief overview
of core animation here.
And you're probably already
familiar with CALayer.
And this is an object
that contains lots
of properties very similar
to MSView, it has like bounds
and position and opacity.
And these are all properties
that Core Animation
knows how to animate.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
And if you want to use
explicit animation you want
to tell core animation
to animate this,
you don't modify the
property, you tell the layer
to add an animation that
will look kind of like this.
Here we construct
another CABasicAnimation.
We set its from value.
We set its to value.
And we add it to the
animations on that layer.
Well what does that do?
Well first it doesn't
do anything destructive.
It's not actually setting
the property on the layer.
It just temporarily
overrides that property
for rendering purposes.
So what are the consequences
of this?
If you take a look at
this snippet here we have
that basic animation again and
we're interpolating from .75
to .25 and it really
is only applying
that for rendering purposes.
If you actually ask the layer
what it is while it's doing its
interpolation between
.75 and .25,
it's going to tell you this.
It's going to tell you it's
opacity is a default value of 1
because nothing has
actually changed
that property inside the layer.
So again we have to
have implicit animation
for core animation as well,
why have one of everything
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
when we can have more than one?
So in this case animations
are avid
in response to property changes.
If you call this code
you're setting your property
on your layer, you actually
get all of this for free,
which is kind of handy.
It makes the basic
animation for you.
It figures out that the two
values, what you're setting it
from and what the from value
is and what's already on screen
and then it adds it as an
animation to your layer.
If you're curious about that
there's more information on this
in the CAAction Protocol which
lives inside of CALayer.h,
you can do some pretty
clever things
but that's beyond the scope of
what we're talking about here.
Tying this back into
views though,
how does this effect
layer backed views?
Well first of all views can
optionally delegate a lot
of the drawing of animating
responsibility to CALayer.
And you would generally get this
by studying the wantsLayer
property of your view to yes.
And what that does is
it causes your view
to manage its own CALayer
and it also causes all
of its descendants to
manage their own CALayers.
And there's a few reasons you
might do this besides animation,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
performance and memory are the
big ones, and they trade off,
sometimes non very predictable
ways but we trust you
to balance it for your
own applications needs.
The take away from
this though is
when you're using
a layer back view,
it's going to behave
a little differently
than a non-layer
back view, surprise.
If you want to know more
about these differences
I encourage you to stick
around for our next session,
215, Optimizing Drawing
and Scrolling on OS X.
I realize this slide says this
was 11 hours ago and we did
in fact run the session 11
hours ago but we're going
to run it again in 40
minutes or so just for you.
[laughter and applause]
So how does AppKit run the
animation for non-layer
or even layer backed view
over not delegating
our responsibility
over core animation?
Well first of all we keep
this animation around.
It doesn't immediately change
the property in your view.
And we periodically wakeup the
main thread to do our animation.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Every time we wakeup we're
going to evaluate that animation
for the current time and figure
out what the current value is
and we're going to apply it
to the object that's being
animated, in this case the view.
So we're actually replacing
the property inside the view,
that means you'll see all
of your key value
accessors get called,
all the change notifications
get called,
all the NS notifications
get posted.
And the drawing of this
property actually just happens
as a side effect of the
regular NSView drawing cycle.
And when we change a property
that effects the visual
appearance of NSView,
the view gets marked as
needing display and we draw
that on the next from loop pass.
So here's an example, if you
do an animation you're going
to see this happening,
the main thread wakes up
and every time it wakes up it
changes the size of a view.
Core animation works
a little differently
so it also stores the animation
in addition to property
on the layer so that's similar.
And this is somewhat
nebulously phrased, it is waking
up periodically on a background
thread or maybe another process
but it does different
things from this point on.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
It evaluates the animation
but only evaluates it as part
of rendering so the results
you see on screen aren't going
to be back propagated
into your layer.
The property of the layer is
left unchanged and unmodified.
So suppose you're
running the same animation
with a layered back view and we
delegate this to core animation.
You're going to notice hey
the size on screen is changing
but we're never doing
anything on the main thread.
None of those key
value accessors
or notifications
are being posted
so that's an important
difference.
In general we try to let
core animation drive things
because doing stuff
in the background is
usually more performant
and lets us be more generous
with what the main
thread can do.
The frame, frame origin
and frame properties
of NSView are important
exceptions to this
where we don't necessarily
let Core Animation drive them.
And our decision is
governed largely by the value
of the layerContentsRedrawPolicy
of your NSView,
and again that will be covered
in greater detail in session 215
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
in this room right
after this session.
So I want to call out
this code snippet here.
This is very simple.
We have a window and we
want it to animate in a view
so we want it to look like that.
We set the views initial frame
and then we told the animator
what its final frame was.
So if we go back to
here the only difference
in the snippet is that
we've told it wants a layer.
If we run that animation
we see this.
Again the screen changed,
it's technically in animation
but it's not a great animation.
It's definitely not what we
wanted so why did that happen?
Well first, Core
Animation groups all
of its changes into
transactions.
And if you're using
implicit animation,
Core Animation is going to try
to grab the on screen value
as your from value but the
view was never on screen
so there wasn't any
on screen value.
That initial value we set on
the view is actually replaced
in that same transaction by the
value we set on the animator
so there's at least two
easy ways to fix this
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
and I'm going to
show them to you.
So in this case we're going back
to manually specifying the from
and to values of our animation.
Again we do that by setting
our animations dictionary
on the view and then telling
the animator to go to the
to value as necessary.
Technically we will
automatically fill in the from
and to values for
you, in this case the
from value is the
important part to specify.
The to value is inferred from
what we're setting the frame
on the animator to so it's
optional but leaving it
in there is often
good for clarity.
The other way of doing this is
with animation groups again.
And in this we actually
use our changes block
in the animation group to set
the initial value on the view
and then we use our completion
handler to tell the animator
to set it to the to value.
And in both cases we're going
to get an animation looking
like this, which is
exactly what we want.
So at this point I'd like to
welcome my colleague Peter Ammon
on stage.
He's going to get you
started with Auto Layout.
Welcome Peter.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
[ Applause ]
>> Peter Ammon: Thanks, Chris.
I'm Peter Ammon.
I'm a Cocoa Frameworks Engineer.
I apologize my voice
is a little rough.
I'm fighting off a cold.
We'll be talking
about a new class
in AppKit called NSStackView
which is really cool
and then I'll show you
three different techniques
for doing animations in
an auto layout based app.
So NSStackView, let's say you
just have a collection of views,
a button, a text field, a label,
you want to put them all
together into a list or a stack,
well that's what
NSStackView does for you.
Pretty simple, right?
Where it gets its flexibility
and its power is from its use
of auto layout, everything
is strung together
with constraints.
That means it knows how things
should be sized according
to their intrinsic content size
or any other constraints
you apply.
It knows how things
should be aligned
so you can specify a top
alignment, left alignment,
here we have a baseline
alignment.
And it interacts well
with window resizing
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
so you can make a stack
view that prevents you
from resizing the window
too big or too small
so the things wouldn't
clip, etcetera.
And this is a really common
type of layout to have
and StackView makes it very easy
to create so you can go back
to [inaudible] or Mountain
Lion and create these yourself
but with StackView
it's a lot easier.
We figure why stop there?
We can make a stack view
that's horizontal or vertical.
You can have one that
has flexible spacing
or equal spacing.
Equal spacing in particular
is difficult to create
with auto layout today.
You can make a StackView
nestable
so you can put a stack
inside another one, why not?
And you can have a stack view
that will automatically
have views that get thrown
out when the StackView
gets too small or put back
in when the StackView
has enough space.
For example, how
NSToolbar does it.
And the API for StackView
could not be simpler.
You can create a StackView with
just passing an array of views
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
to NSStackView, StackView
with Views and you're done.
So we're going to use StackView
to create an inspector
type window.
You may have seen this
in FindersGetInfo or in
for example a graphics editor.
For each of the individual
views in our inspector window,
for example filters, we're going
to string it all
together with auto layout.
So we have constraints
that specify the positions.
And you'll notice the
bottom constraint is dashed.
That means it's breakable.
It has a priority
less than required
and that means it could be
overridden by a constraint
with a higher priority.
So when we want to
disclose it, when we want
to collapse this view, we're
going to add another constraint
which just says center the
label in the view, that's going
to force the bottom
up until it's centered
and that's how we collapse it.
And this way we're going
to put these in a StackView
and that's how we'll get
the inspector window.
So I'm going to show
you a demo of that.
So here's the views that we're
going to put in our StackView.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
We have the header view.
We've got three views that
are all kind of similar
and I'll zoom in
here and you'll see
that all the labels here are
positioned using auto layout.
I use the new X Code 5
Auto Layout Work Code
which is a lot nicer,
for those of you
who attended the session earlier
here today you saw that too.
And you'll notice the bottom
constraint has a priority
which is still pretty high,
725 but it's not required,
which means that a higher
priority constraint can
break it.
And by the way, one of these
views is itself a StackView.
You'll notice that we
have, this is a StackView.
We have some basic support
for configuring a StackView
on the interface builder,
in the seed build.
But in a future release
of X Code hopefully GM
we will have full support
for configuring the Stack View
with all the views inside it.
So now I'm going to switch
to the window controller.
So window did load.
We're going to start by making
a list of all our views,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
the header, the filter,
the shapes view.
And we're going to
make a StackView.
We're going to say
that it's vertical.
Alignment is leading, that
means that it's left aligned.
They all have the same
width so it doesn't matter
but if we wanted everything
to be left aligned,
that's what we would do.
And there's no spacing
between them.
We're going to see oh it
keeps its width and its height
with a high priority and
we're going to set it
as a document view of
a certain scroll view,
of the scroll view
that's in the window.
Next we're going to tell it
how do we position ourselves
in the scroll view?
Oh using the Visual Format
Language we're going to say
if penned to the
left and the right
so it just fills that width.
Vertically its penned to the
top, that's what that means,
but it's not penned
to the bottom.
It can float freely.
And this has to be in a flipped
clipped view, by the way.
So in our disclosure
view this is the view
that goes into the StackView.
We'll start by creating
a constraint.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
This is the centering
constraint that I showed you.
We're seeing the label
fields center Y is equal
to our center Y times 1 plus
0 so it's exactly centered.
Now when it comes time to
collapse it we're going
to remove that constraint, I'm
sorry, we want to uncollapse it,
we remove the constraint, set
the title of our button and say
that we're no longer collapsed.
And when we want to collapse
it we add the constraint,
set the title and say
that we are collapsed.
So that's not a lot of code
but here's the affects you get.
Here's our StackView
and you'll see
that I can disclose
things pretty nice
and I can resize the window,
for example, I can scroll.
So we got this pretty
sophisticated control
without a lot of code.
But this is a talk on animation
so how would we make this
disclosure view animate?
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Well the desire is that
we want to just switch
from our old layout
to the new layout.
We want to just add that
constraint or remove it.
But rather than the views just
jumping we want them to animate
from their old position
to the new position.
So Chris showed you some
techniques for doing this
in a non-auto layout based
app where you set the frame
through the animator
proxy or you set the frame
after setting
allowsImplicitAnimation to yes.
But with constraints
you're never supposed
to set frames directly
so how would we do
this with constraints?
Well here's some things people
have tried that don't work.
What they have done is they've
tried opening the animation
block and saying add constraint
and you'll see that
doesn't animate.
They've tried adding
a constraint
and maybe setting the constant
to something, that
doesn't animate.
And here's something people have
tried, they're actually going
to call layout on the view
after modifying the constraints.
And this might seem to work
but this is really bad,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
you can see it's
off the page bad.
Why does it seem to work?
The underlying step frame calls
must be in an animation block
with allows implicit
animation set to yes.
And that's actually
where those calls occur
in the layout method.
But the layout method
is for overriding.
It's not a method you're ever
supposed to invoke yourself,
unless you're invoking super.
So how do we trigger it
being called by AppKit?
Well we're going to start
by adding the constraint.
We can do that inside or
outside the animation block,
it doesn't matter but
here we do it outside just
to make it clear.
Or you can set the
constant on a constraint.
If you're not familiar
with a constant
on a width constraint
it's just the width,
or it's like the base value.
For a spacing constraint,
it's the space
between the two views, etcetera.
And then in the animation
block we're going
to say allows implicit
animation yes.
And we'll trigger layout
by calling view
layoutSubtreeIfNeeded.
If you just want to animate that
view and all of its descendants
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
or you can call window
layoutIfNeeded
to animate the entire window.
So this is the right
way to do or one way
to do animation correctly
in an auto layout based app.
So let's make our disclosure
view animated in this way.
So I'm already in the right
spot, toggle collapsed,
and I've already added
or removed the constraint
so now all that's left to do is
open the animation block here.
Say we allow implicit
animation yes.
And then trigger layout if
needed on the entire window.
So now when I run this
you'll see that instead
of just jumping the views
actually animate open
and close like that.
So just a few lines of code we
went from this very static UI
to a nicely animated control.
[ Applause ]
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
It's nice when things
take a little code.
So that's one technique where
you just set your constraints
and you trigger layout
in an animation block.
But another technique is to
animate constraints directly.
As I said, constraints have
only one mutable property,
once the constraint
has been installed
on the view there's only
one thing you can change
about that constraint and we
chose to call that constant.
You can animate it
with the animator proxy
so you can say
constraint.animator.constant=17
and this uses the same
sort of animations
that Chris was telling
you about.
I know the constraints do not
respect allowsImplicitAnimation
so the constants will
always, you either have
to use the animator proxy
if you want animation.
This is because if
they did respect
that allowsImplicitAnimation
you might get layouts
which are not allowed
which will cause conflicts
with other constraints.
So let's, I'm going to
give you a demo of how
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
to use this technique to make
our StackView animated again.
So it was kind of
nice that it animated
but the window didn't
change its height, right,
so you have to scroll down or
you have to resize the window.
It would be a lot nicer if after
you disclose it the window just
resized and matched the
height of the StackView.
So we're going to do that
by instead of just penning
to the top we're also going to
pen to the top and the bottom.
And because our layout
priority is high
for the vertical orientation,
it's higher than the strength
with which the window
holds its size
so this will actually
push the window taller
or force it to shrink.
Here's the other implementation
of where we're going
to animate the constraint.
Instead of that centering
constraint we're going
to have a constraint which
just says what is the height
of my view, of me?
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So it's equal to
nil, not an attribute
because we're just setting
the height to a constant
and that's going to be
either the collapsed height,
which is just the
height of our top part,
or our uncollapsed height
which is our fitting size,
the height we want to be.
And then we're going
to add that constraint
and then we set the
title to another button.
So then the toggle collapsed.
Instead of triggering layout
with the animation block,
we're going to go
through the animator proxy
and set the constant to
either the uncollapsed height
or the collapsed height.
So now when I run
this you'll see
that not only does it animate
but it also resizes the
window bigger or smaller.
So you may be wondering why did
we use this separate technique?
Why couldn't we have just made
the window resized using the old
technique for animation?
Well I'll show you what
happens if I do that.
So I if that out, now we're back
to doing the implicit animation
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
and triggering layout.
When I run this you'll see
that it, it has this weird kind
of jumpy behavior here.
So what's going on with that?
So what we saw there was a core
animation driven animation.
It's using layers concurrent
with the window resize
animation,
which always happens
on the main thread.
So when you have these
two animations going
at the same time they may get
out of sync or they may fight.
We say don't cross the streams
and if you do something terrible
will happen or not quite
that bad but you'll get
drifting or you'll get jitter.
You'll see a view maybe
vibrate a bit and this is a clue
that there's two
animations going
at once on different threads.
So the solution to
this, one solution,
is to go through the
constraints animator
that will do an AppKit driven
animation on the main thread
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
so it will not be concurrent
with the window resize.
Or you can do the
other technique
where you can tell the window
to just resize itself
in an animated way.
And that's the third
way of doing animations
with auto layout, which I'm
going to talk about now.
Animating window size changes,
so we're going to switch
to a totally different
kind of application.
This is a shopping list app
where we have a master
and a detail window.
So here we have a shopping list
and there's a segmented
control at the bottom right.
And when I click
that what I want
to have happen is a
side bar will slide out
and the user can select
different shopping lists.
So you may have seen this type
of UI in reminders or notes app.
So when the user clicks
that segmented control,
we want the window to grow but
it grows in a different way
than if the user had
just resized the window.
When the user resizes it we want
the pane on the right to pick
up this extra space,
all the slack.
But with this kind of animation
what we want is instead all the
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
slack should be taken up
by the pane on the left.
So how are we going
to accomplish this
animation using auto layout?
Well the trick is we're going
to set temporary constraints
that control how the panes
resize so we're going
to grow the window and
remove those constraints.
But we don't have to add
these constraints ourselves.
We can leverage NSSplitView,
which is a great client
and auto layout starting
in Mountain Lion,
to create these constraints
for us.
We do that by adjusting
the holding priority
of the split view panes.
The holding priority
is the strength
of which the split view pane
prefers to hold its size
so as the window
gets bigger the pane
with the lowest holding priority
will take up all the slack.
So we start by getting
the window framed.
And we're just going to
adjust the origin left by 120
and adjust the width by 120
so the right edge stays fixed
and the window gets bigger.
We're going to lower the holding
priority of the first subview
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
to 1, which is very low.
We'll then set the frame of
the window and we're going
to pass yes for animate.
Instead of just jumping it's
going to cause it to slide out.
And then we'll restore
the holding priority
of the split view pane.
So this gets us almost
all the way there
but there's one thing
we can't quite do,
one remaining wrinkle,
what's that?
We want not just the
pane to disappear
but also the split view divider.
And a split view calls
this collapsing the pane
so this is something
it has support for,
which we want to
take advantage of.
But collapsing is a
deliberate user action.
There's a special way the
user collapses a pane,
it's by dragging more
than halfway across it
so it's not something
constraints can just do
on their own.
However, oh, excuse me,
so here's what constraints
can get you,
they can get almost all the
way down to the last pixel
but to get it the rest
of the way we need
to invoke NSSplitView.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So the solution is we're going
to shrink the pane as small
as we can get using auto layout.
Then we're going to collapse it
by calling setPosition:0
ofDividerAtIndex:0.
That says take the divider
and push it all the way
over to the left, which
will cause that pane
on the left to collapse.
And to uncollapse it is
almost exactly the same except
for backwards.
We start by setting
the position to 1
and then we grow the
pane using auto layout.
And don't forget we have
to enable collapsing.
We do that by implementing
the delegate method,
SplitView canCollapseSubview
and say oh you can collapse it
if the subview is the first
subview in a split view
so let's give a demo of that.
So here's our master detail view
and we have the segmented
control down here.
When I click that you'll see
that the split view
slides nicely in and out
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
and I can you know
resize it how I like
and then I can collapse
it and collapse it again.
How do we accomplish this
in our split view demo?
So here's the toggle collapse.
This is the action method
of the segmented control.
We start by getting the first
subview of the split view
and that's what we
want to collapse.
We say that do we want to
collapse it or uncollapse it?
And if it's already
collapsed the way we want it,
we just return.
So we're going to get
the holding priorities
of the first subview and the
second subview and we're going
to lower the first one and raise
the second one and we're going
to restore them at the end.
Now we're going to call that set
frame display animate method.
So we start by, if
we're collapsing,
if we already collapsed,
excuse me,
we're going to set the
position to just 1,
1 of the divider
index so that's going
to uncollapse it by one pixel.
Then we have to resize
the window by that amount
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
so that the right
edge stays fixed.
Then we're going
to do it the rest
of the way just using a setFrame
display, yes, animate, yes.
And likewise if the window
is already big we want
to make it small, we
want to collapse it.
We start by, excuse me, we
get the frame, we say yes,
animate yes and then
we, excuse me,
this is using auto layout here,
then to collapse it
we set the position 0
of the divider index zero
and resize the window
in a non-animated way
for just that last pixel.
And then we restore
the holding priorities
to what they were before so that
when the user resizes it they
get the resizing behavior
we want.
And here's where we implement
the canCollapseSubview method
to indicate that we can
collapse that first subview.
So that's how we
get that effect.
So that's what we have.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
For more information you
can contact our Frameworks
Evangelist Jake Behrens.
There's also really
good documentation
on Core Animation
and Auto Layout.
You can always reach us at
the Apple Development Forums.
We've got some really
good labs too,
oh later sessions,
related sessions.
Hopefully you saw the really
nice auto layout session
for interface builder
that was earlier today.
And there's going to be a
session immediately following
this about Optimizing Drawing
and Scrolling using some
of that responsive
scrolling stuff,
immediately following
in this room.
So what did we see?
We saw NSStackView, how it
can layout views in a list,
horizontally or vertically.
It's a really powerful
new class in Mac OS X.
We can animate view positions
by adjusting constraints
and triggering layout
within an animation block.
By using layers you can
produce very smooth animations.
We saw how you can animate
constraints directly
by using the animator
proxy NSLayout constraint
and this allows you
to resize windows.
We saw a third technique
which is you set
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
up your constraints
the way you want
and then you tell the window to
resize then pass yes for animate
and then the animation will
cause the content to reflow
in the way you specify.
But be careful to not
use core animation,
driven animations
alongside window resizing,
we call that crossing
the streams.
You'll get that jitter
or you'll get drifting.
Thank you very much.
Enjoy the rest of your week.
[ Applause ]