Advanced Animations with UIKit

Session 230 WWDC 2017

So much power has been added to animations on iOS since their inception that it's time to think about animations in a whole new way! Learn to combine and coordinate between multiple animations, resulting in interactive transitions and learn some tips and tricks along the way.

[ Background Sounds ]

Hey everyone, I'm Joe Cerra and I'm an engineer UIKit and welcome to advanced animations with UIKit.

So, today we have a lot to talk about.

We're going to start off by covering some of the basics and how animations work and also how animations are timed.

We're going to discuss how to make animations fully interactive and interruptible using some modern techniques.

Then we're going to talk about some of the new API that we're providing this year in iOS 11.

And then we're going to use all of that knowledge and I'm going to show you how to coordinate animations.

And then finally, we're going to go over some tips and tricks and a few more techniques to help you create really great animations for your users.

So, let's get into it.

UIView-based animations have been around really since the dawn of iOS.

And here's a quick refresher on how they work.

So, say I have a UIView that renders a circle and I'd like the animate it from its position of X equals zero to 100.

Well the way I might do this is by calling the UIView.animate method.

Here I'm calling UIView.animate with a duration, performing some animations in an animation block.

In this case, I'm offsetting the X value of our circle by 100.

And when I do that UIKit will implicitly create an animation, add it to our layer and produce the following animation.

But last year we introduced the UIViewPropertyAnimator, which gives us a lot more control for animations than its predecessor UIViewAnimate.

Now this includes being able to provide custom timing functions and the ability to make your animations fully interactive and interruptible really easily.

In fact, with property animator you can modify your animations on the fly.

So, let's take a look at our animation again and this time we're going to use an animator to animate it.

So, we create our animator providing a duration and a timing curve provider animations.

And then we call startAnimator which actually then runs that animation block producing this animation.

Now in the last two examples we drove our animation with a linear timing curve.

And what a timing curve is, is essentially a function that maps time to progress or the fraction of elapsed time of your animation to the fraction of progress of your animation.

Now linear timing curves are actually kind of interesting and this is because the fraction of time is equal to the fraction of progress and we'll see why this is interesting in just a second.

There are also of course, timing curves other than linear, such as the built-in ease in timing function which starts off slowly and then accelerates.

And the built-in ease out timing function which starts off quickly and then decelerates.

Well with property animators you can provide your own custom timing functions, such as this one.

And the way you do that is by providing two cubic Bezier control points.

Now we'll see a little bit later why it can be really useful to be able to provide your own custom timing functions.

So that's essentially how animations work.

Now I want to talk about how to make your animations fully interactive.

So, an interactive animation is one in which the user's actions interactively drive the progress of your animation.

So, here's a familiar example where a gesture is driving interactive animation and now we're using 3D Touch to drive another animation.

And we can, of course, dismiss our Control Center view interactively with a gesture.

So, let's do a quick demonstration here.

So, we're going to add a PanGestureRecognizer to our circle and we're going to animate it, but we're going to do so with our finger by scrubbing an animator.

And at this point we're going to lift our finger off and continue that animation to its target position.

Now here's the code to do this.

We're going to save an instance of our property animator in our hand gesture recognizer handler.

We're going to create that animator and initialize it with some animations in an ease out timing function.

Afterwards we're going to call pauseAnimation immediately after and that's going to run that animation block producing that animation implicitly.

And what property animator does is essentially sets the speed of that animation to zero.

So now we can interact with it.

So, we're going to scrub our animator's fraction complete based on the distance our finger travels relative to the total distance of the animation, which in this case is 100 because we're animating from zero to 100.

And when our finger is lifted off we're just going to call continueAnimation.

Now that's really easy, but there's actually two really interesting moments that occur here and that is when we pause our animation and then when we continue it, so let's take a look at those.

So, we've just created our animator and we're about to pause it so we can interact with it.

Notice that our animator has been created with an ease out timing function.

So, let's call pause and see what happens.

So, our animator becomes active, but we've just converted our timing curve automatically into a linear timing curve.

Why did the property animator do that?

Well it turns out this makes scrubbing your animation really easy because of that property of linear timing functions where the fraction of time is equal to the fraction of progress, you can now scrub both time and progress uniformly.

Now let's take a look at what happens when we continue our animation.

So, we're scrubbing our animation here and now our finger is lifted off and we're about call continueAnimation, so let's see what that does.

So, we convert back to that ease out timing function, but also something interesting happened.

We remapped time in the process.

So, fraction complete which was once 50% is now 10% and the reason for that is we want to keep our progress value stable when we convert back to that ease out timing function.

Also, I want to draw attention to our duration factor here, which is zero and what this means is for our property animator to use whatever remaining time it has left, which in this case would be 90% of its original duration.

So, if for example, our animator was created with a duration of two seconds it would continue in 1.8 seconds.

So that's how interactive animations work.

Now let's talk about how to make our animations fully interruptible.

Now an interruptible animation is one in which the user's actions interrupt or pause a currently running animation.

Here's an example that you're probably familiar with, with Safari.

When you flick your finger, it accelerates and then it sort of decelerates.

But if you touch the screen again you interrupt that animation at which point you can scrub it.

So, we're going to do one more demonstration this time and we're going to add our PanGestureRecognizer to our circle, but this time we're going to let it animate for a little bit and then we're going to catch it midflight.

So here it is animating then we catch it with our finger at this point we can scrub it, but we've already seen how that works so instead, we're just going to lift our finger and continue the animation to its target destination.

So, here's the code from before and we're just going to make a few changes so we can support both interactive and interruptible animations.

So, we're going to introduce a new method here called animateTransitionIfNeeded and this is a custom method, this is not a UIKit method for example.

So, what this does is it'll initiate our transition if it isn't already running.

Were also going to introduce a new property called progressWhenInterrupted and this is going to save any relative progress made by your animator prior to it being interrupted.

Now when our gesture begins we're going to create that animator again, but this time only if our transition isn't running.

We're then going to pause it so that we can interact with it and we're going to save any relative progress made by it prior to being interrupted.

When our finger moves we're going to scrub our animator's fraction complete, but this time we're going to scrub our animator based on the distance our finger has traveled relative to any progress it made prior to being interrupted.

And when our finger lifts we're going to continue our animation.

But to make this example a little bit more interesting let's continue with an ease out timing function and let's assume that our animation was created with an ease and timing function just to see what that does.

So, here we are about to interrupt our animation and fraction completes about 50% sol it's ran for about half its duration and progress is only about 10% because we're on an ease out timing function.

So, let's call pause and watch what happens.

So again, we convert into that linear timing function to make scrubbing really easy, but we also have remapped time just as we did before to keep progress stable so our animation doesn't jump.

Now when we call continueAnimation we're going to do so with an ease out timing function and again, you can see we convert back into that new timing function and remap time again.

So, this is a really subtle aspect of property animators that's really important to understand when you're manipulating them.

So that's how to make animations fully interactive and interruptible.

Now let's talk about some of the new API that we're providing this year.

So, new in iOS 11 property animator is getting two new properties, scrubsLinearly and pausesOnCompletion.

It's also getting a new behavior which is starting as paused, so let's talk about these.

So, in the last couple of examples you saw that when we paused our animator it converts the timing function into a linear timing function and it does this to make scrubbing the progress of your animator really easy.

But you know sometimes it's really useful for your animator to maintain its pacing when it's being driven interactively.

And now you can do that by disabling linear scrubbing.

And here's just a quick example where linearly scrubbing the opacity of the circle on top and we're nonlinearly scrubbing the opacity of the circle on the bottom according to an ease out timing function.

We'll see a little bit later when we're coordinating transitions this could be a really interesting thing to do to create some pretty compelling animations.

Animators can now also pause on completion.

So, when an animator's animations finish it will automatically transition into the inactive state.

And when it does that it's going to release any animations that it was previously tracking which means you cannot manipulate or even reverse them after they've finished.

But now if you set pauses on completion to true your animator will pause at 100% fraction complete allowing you to at any point in the future reverse those animations.

And to give you guys a little bit of insight here we actually use this in UIKit for Drag-and-Drop.

So, here's an example of a lift animation and as you may be aware, you can provide your own alongside animations to customize this.

And we're actually going to drive those animations by a property animator that pauses on completion internally in UIKit.

And because of that we could easily reverse those animations for you at any time after the user lifts their finger even if your animations have already finished.

I also should mention that we're not going to call your animator's completion handler when it pauses on completion, but if you are interested to know when those animations have finished you can simply observe the running property.

And finally, now you can create a property animator and start it before you've provided any animations to it.

Now what this does is any animation blocks that you subsequently provide will be ran immediately instead of escaping.

And this is great if you're transitioning any of your UIView animate code over to using property animators.

Now I just want to spend a couple minutes here talking about springs.

So, spring animations are pervasive in UIKit in iOS and they add a sense of realism to your animations.

Now in UIKit we provide two kinds of springs, critically damped springs and under damped springs.

A critically damped spring is one which accelerates quickly and it sort of decelerates just as quickly hovering over its target value, whereas an under damped spring accelerates quickly beyond its target value and then oscillates.

So, spring animations are unique in this way and we think of spring animations just as we think about timing curves.

But spring animations are also unique in that they always animate from their current presentation value.

And there are a couple reasons why we do this.

So, the first is, it may actually be undefined to convert a spring animation to a cubic animation and this is because cubic timing functions don't oscillate or overshoot their value as they're bounded by minimum and maximum values.

And the second reason it's a little bit more interesting and this is if you animate with a two-dimensional initial velocity with unique DX and DY components property animator is actually going to decompose that for you and create two spring animations.

And because those two spring animations have different velocities they're going to be desynchronized so we're not going to be able to convert those onto a cubic animation.

So, if you do have to interrupt the spring animation here are a few best practices.

So, the first thing you can do is consider stopping your spring animation, promoting current presentation value to model value, and then creating a brand-new animation from there.

The second thing you can do is consider using a critically damped spring without initial velocity as these don't overshoot or oscillate.

And then finally, if you are animating with a two-dimensional initial velocity with unique X and Y components you can consider decomposing that yourself and driving the X animations with one animator and the Y animations with another.

So that's how property animations work.

Now let's talk about how to use all that knowledge to coordinate animations.

So, for this we're going to build a fully interactive interruptible animated transition that coordinates across multiple uniquely timed animators.

So, say that we have an app and our app is showing some piece of content and anchored to the bottom of the screen is the Comments button and when we tap on it it expands showing our comments view.

Now we might implement this using UIView controller animated transitioning for example, but we'd like this to be completely interactive and interruptible so I'd like to show you how to do that.

So, the first thing we're going to do is add two gesture recognizers, a tap gesture recognizer so that we can tap on it and expands and tap on it again it will collapse.

We'd also like to be able to tap on it while that animation is running so it can be reversed.

And we're going to add a pan gesture recognizer so we can interact with it.

So, here's our code form before and we're going to make just a couple modifications to it to create this infrastructure that we're going to build on.

So, the first thing we're going to do is we're going to replace our instance of our property animator with a collection of running property animators.

And for that collection of running property animators if we ever create a property animator we're just going to add it to that collection and let's assume that when those animations finish it's automatically removed.

Next, we're going to reintroduce our animateTransitionIfNeeded method and it's going to take a target state to animate to.

So, if we look at this if our runningAnimators.isEmpty that means there's no transition currently running, so we're going to initiate a transition if that is the case.

And we're going to do that by creating a new property animator for our frame which is going to use a critically damped spring.

We're then going to perform our animations and we're going to start that animator and add it to that collection of running animators.

Next, in our tap gesture recognizer handler we're going to call this method and this is going to animate or reverse our running transition.

So, if our transition isn't running we're going to just initiate our transition, otherwise we're going to iterate through all of our running property animators and reverse them.

Now for these next three methods we're just going to extract our pan gesture recognizer handling code from before, so I'm just going to quickly summarize what these do.

So, for startInteractiveTransition this is called when your gesture begins and it's just going to initiate the transition if it isn't running, pause all of your animators uniformly, and save any relative progress made by them.

UpdateInteractiveTransition is going to scrub your animators uniformly relative to the distance your finger travels and any progress that your animators made prior to being interrupted.

And then finally, when your finger is lifted we just call continue animation on all your animators conditionally reversing them based on the direction your finger was traveling.

So, let's check this out.

So, we can drive our animation non-interactively by tapping, we can also interactively drive it by pulling up or pulling down, and we can tap on it again while it's running to interrupt it and reverse it.

And we can also capture animation while it's running and at which point we can scrub it.

So now we've created this infrastructure and right now we just have a frame animation which isn't that interesting, so let's make this a little bit more interesting.

So, first thing we're going to do is we're going to add an interactive blur.

Now in iOS 8 we introduced UIVisualEffectView, which allows us to add blur and vibrancy to our view hierarchy.

And it turns out that the effects property of VisualEffectsView is an animatable property so that's great.

So, what we're going to do here is the only code changes we're going to make is to animate transition if needed and we're just going to create a new animator for our blur, which is going to use a critically damped spring at least for now.

And then we're going to perform our animations here either setting or unsetting the blur effect, starting our blur animator and adding it to that collection and here's what we get.

So, we can now have an interactive blur animation, but.

Let's see that again in slow motion because I've got to be honest with you guys, I don't know if I'm really feeling this blur animation right now.

I feel like it's animating potentially a little bit too quickly.

Now let's drive this and take a look at it.

So maybe it looks a little better, but it still doesn't look quite right.

Now there are a few reasons for this.

The first is because we're using a critically damped spring our blur is going to animate in too quickly.

And because our property animator is going to linearly scrub it our blur is still going to animate in a little bit too quickly and it's going animate out too slowly.

So, in order to fix this, we're actually going to provide our own custom timing curves.

So, our custom ease in function so that our blur animates in really slowly and a custom ease out timing function so it animates out really quickly.

And because these are inversions of each other we're going to get symmetric pacing, which means that the path of our animation on its way out will match that of its path on the way in.

So, here's the code for this, we're just creating cubic timing parameters based on the target state and we're going to disable linear scrubbing here so that our animator maintains its pacing when we're driving it interactively.

So, let's check this out again.

Much better.

It's really subtle, but it's much better this time.

And let's also do that a little bit more slowly so you can really get a sense of what it looks like.

And also, let's drive that interactively so you can see that it does indeed maintain its pacing when you're driving it interactively.

Cool, so now we have two property animators with unique timing characteristics contributing to our overall transition.

But let's make it a little bit more interesting.

I'd now like to demonstrate a technique I like to call view morphing.

So, say that we have a label; in this case we have a label.

It's blue, it's kind of small, it's got a regular typeface and let's say we want it to be a lot bigger, maybe a different color, maybe a heavier typeface.

What would that transition look like?

Well this is what I call view morphing, it's the scaling translation and opacity blending of two views.

So, in this case, we're going to use UILabels, but this technique is generally applicable to any view or view hierarchy; not just labels.

So, [inaudible] we're going to take our comments label and we're going to make it blue and when it expands we're going to want it to look like this.

And notice it's much darker, but it's also slightly inset from the top of its parent view.

And the way that we're going to animate that is like this.

So, how are we going to build this?

Well, UILabels don't expose any animatable properties, but that's okay because as I mentioned, this is generally application to any view or view hierarchy, not just labels.

What we're going to do is use UIView's oft overlooked transform property and we're going to compute the scale and translation for both of our labels so that we can blend them into each other.

And we're also going to animate their opacities so that we can blend them together.

So, first thing we're going to do is compute the scale.

It turns out this is really easy to do, it's just a simple dimensional ratio based on the target dimension and your current dimension.

And, in fact, once you computed one of these you basically get the other one for free by taking the inverse.

Now computing translations is a little bit more interesting and this is because we're animating our scale, which is going to affect our bounds during the transition, so we can't just simply take the Y offsets.

What we can do is pre-apply that scale of transform in order to obtain a new value for our Y offsets and we could use that for our translation.

And now we're going to drive this using three animators, a critically damped spring to drive our transform so that it follows the overall path of the transition and then ease in and ease out animators to perform the opacity blending, both of which are going to scrub nonlinearly.

And here's the code for this and I've also omitted some of the repetitive bits.

So, we created transform animator, we animate the transform of our labels.

The incoming label is getting the identity transform and that's because it's already been pre-scaled and translated down to match the bounds of the outgoing label.

And the outgoing label is going to get transformed such that it matches the bounds of the incoming label prior to it being animated.

And then we create two property animators here to blend our alphas, we disable linear scrubbing here so they maintain their pacing and let's see what that gets us.

So again, non-interactively we can animate this transition, but we can also interactively animate it and it looks great.

And we can even animate it and then interrupt it and it just works.

What I think is really cool [ Applause ]

What I think is really cool about this is we now have one cohesive animated transition that's being driven by six different property animators, five of which have their own timing characteristics.

Now being able to do this prior to property animators required a ton of code and complexity, but now with this infrastructure we've set up we can use property animators to easily achieve these effects just by declaring our timing characteristics and scrubbing behaviors.

So that's how to coordinate animations.

Now let's talk about some tips and tricks and some additional techniques that you can use when you're creating animations for users.

So, is there anyone out there that's ever tried to animate a corner radius before?

I imagine maybe a few of you.

So, to do this you pretty much have to manually set your corner radius or if you want to animate it you have to create a CA basic animation and set its to and from values.

What we'd like to do is animate our corner radius such that it's interactive.

So, how are we going to do this?

Well I'm really happy to let you guys know that corner radius is now a fully animatable property in UIKit.

[ Applause ]

Now because UIView doesn't expose the corner radius property you actually have to reach into your views backing layer to modify the corner radius, but as long as you do that within an animation block we're going to implicitly create that animation for you and it'll be fully trackable and scrubbable.

In fact, you can even do this from the UIView animate method.

So that takes care of our interactive corner radius animation.

Now what about these two guys, we only want to animate the top left and right corners, so how do we do that?

Well I'm also happy to let you guys know we're adding a new property to CALayer, which is maskedCorners.

[ Applause ]

Now this allows us to selectively choose which corners we want to apply our corner radius mask to, which in our case is going to be the top left and top right corners.

And now finally, here's the code to do this again, omitting some of the repetitive bits.

So, we're just creating a new animator here, we're going to use a linear timing function and we're going to perform our corner radius animations and that's pretty much all we need to and that gives us the following animation.

And it's subtle, but we are indeed interactively scrubbing the corner radius there, which is cool.

Now if there's one underlying message thus far it's that it's really important for all your animators to share the same unit duration.

This makes scrubbing them really easy and it makes it possible to uniformly scrub them.

But, you know, sometimes it's kind of useful to have an animation that finishes a little early or one that maybe starts with a delay.

And an example this could be in the following animation where if you notice the Details button here it animates in about halfway through is when it starts.

And it's fully animated out around that same halfway point.

And if we try this interactively you can really see it, so it just starts animating at this point and it's fully animated out right around there.

And if you look across UIKit you can actually see a lot of places where we do this.

In fact, UINavigationBar since iOS 7 has had this effect when you drive that animation interactively.

So, we could create an animator with reduced duration or we could use a delay factor here, but that's really going to complicate our scrubbing code.

It turns out there's a much more elegant solution here and that is using keyframe animations.

So, if we look at the UIView headers we see the following two keyframe methods.

And I'd like to draw your attention to RelativeStartTime and RelativeDuration.

So, in order to create this effect, we perform a keyframe animation inside of our property animator, so we call UIView.animateKeyframes with a relative duration of zero.

And what this means is our keyframe animation will inherit the duration of its outer property animator and in fact, if you nest animations like this you'll get this inheritance behavior for free.

So, this can be really useful if you're not using property animators.

Now when we expand our comments view we want that animation to start late so we're going to use a relative start time of 0.5 and we're going to compensate for that by using a relative duration of 0.5.

And when we collapse it we're going to use a relative start time of zero because we want it to immediately begin or we use a relative duration of 0.5 so that it finishes early.

And I'd like to draw your attention to these two parameters here.

So, you don't have to use a linear timing function for your keyframe animations you can use any timing function you want, including your own custom timing functions.

And if you have multiple keyframes here, which we don't, you could actually interpolate them using the options parameter or control the way that they're interpolated using the options parameter.

Now lastly, I'd like to talk a little bit about additive animations.

So, additive animations are really powerful and I find them really interesting because they allow us to modify or to animate a single property of a view with multiple simultaneously running animations.

Now for this demonstration the sort of point of it is that I'd like to demonstrate how to think a little bit more additively when we're designing and building our animations.

So, let's say that we have a square and let's say that we'd like to animate it by 360 degrees times 10.

That animation might look something like this.

Now you might be thinking well that's just as easy as animating the transform our view and you wouldn't actually be wrong.

But code such as this, which is animating our view by 20 pi radians, does not produce that animation.

It actually produces this animation.

Did you see it?

Well, it actually didn't animate and there's a really good reason for that.

That's because Core Animation only cares about the total displacement when you're animating the transform of your view.

So, in this case, the target rotational value, which is 20 pi radians, is the same orientation as the current rotational value.

So, the total displacement there is actually zero.

Now Core Animation will actually produce an animation for you, but it's to and from values are going to be the same.

And you know a similar problem exists if you try to rotate a view by 180 degrees counterclockwise by specifying negative pi radians.

And that's because Core Animation because it only cares about total displacement is going to look for the shortest path, which when ambiguous, which it is here, will be clockwise.

So, how do we do this?

Well we could drop down a core animation and create our own CA basic animation and manually set our to and from values and that's perfectly fine and would work.

But we wouldn't get the tracking and scrubbing behavior from property animators.

And it also would make this example much less interesting.

What we could do is we could decompose this into several smaller additive rotational animations and animate them all together to create our desired effect.

So, it turns out transform is an additively animatable property as long as it's affine along with frame, bound, center, and position.

So, in our solution we're actually going to create 20 animations altogether and each animation is going to animate for 180 degrees and altogether they're going to contribute to this following animation.

Now what's kind of cool is we actually have 20 animations running simultaneously here.

And I'm not suggesting that this is a good idea generally, but it does help us think a little bit more additively when we're designing and composing and creating our animations.

So, the point of this is to consider how you can chain many animations together or compose together to create interesting transitions.

So today, we learned about how to make animations fully interactive and interruptible using some modern techniques.

We also talked about how to coordinate several animations together that all have unique timing functions.

And we looked at some techniques in order to help us create some really awesome animations for our users.

It is my hope that you walk away from this presentation and consider making more of your animated transitions fully interactive and interruptible.

So, we have a few related sessions.

If you've missed any of these I encourage you to watch them online and there are a few interesting sessions from prior years.

So, if you're interested in animations, I highly encourage you to check these out, especially last year's session where we introduced UIViewPropertyAnimator.

For more information, please feel free to visit the following URL and thank you all very much.

[ Applause ]

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