Video hosted by Apple at devstreaming-cdn.apple.com

Configure player

Close

WWDC Index does not host video files

If you have access to video files, you can configure a URL pattern to be used in a video player.

URL pattern

preview

Use any of these variables in your URL pattern, the pattern is stored in your browsers' local storage.

$id
ID of session: wwdc2014-236
$eventId
ID of event: wwdc2014
$eventContentId
ID of session without event part: 236
$eventShortId
Shortened ID of event: wwdc14
$year
Year of session: 2014
$extension
Extension of original filename: mov
$filenameAlmostEvery
Filename from "(Almost) Every..." gist: [2014] [Session 236] Building In...

WWDC14 • Session 236

Building Interruptible and Responsive Interactions

Frameworks • iOS • 52:58

Learn how to fluidly transition interactive UI elements from gesture-driven control to animated transitions. Take advantage of new iOS 8 behavior to smoothly transition between several animations on the same view. Discover architectural approaches to interfaces which remain interactive while they animate.

Speakers: Andy Matuschak, Josh Shaffer

Unlisted on Apple Developer site

Transcript

This transcript has potential transcription errors. We are working on an improved version.

All right. Good morning, still, right? Sorry, I've been up here a little while. I forgot what time it is. Welcome to "Building Interruptible and Responsive Interactions". So Andy and I want to spend some time talking about how you can build interactions into your apps that are really fluid and make the user interaction with the content in your app really smooth and natural and avoid any sort of jarring changes as you transition between things. And if you were just here for the Scroll View session, I'm going to start out with something that probably won't be too surprising and show you a scroll view, just to give you an idea of the kind of things that we're talking about here.

So here I've got a nice bird that's in a UIScrollView, and I'm zooming and scrolling and stuff. The thing I want to draw your attention to is the transitions between the user dragging around in a scroll view and letting go and how animations start and then grabbing again as the animations are in progress.

So I can zoom in on this bird and I can drag it down and it'll bounce against the edge. But if I keep grabbing it, there's never a time where I can't start scrolling. It always works. And I can scroll around and it's all a very smooth transition. And that's what we want to talk about this morning is transitions.

So what kinds of transitions? Well, three transitions in particular. Number one: transitioning from gestures into animations and how to do that really, really smoothly. Number two: transitioning from animations to other animations. And I'm sure you've already guessed what number three is: transitioning from animations back into gesture control.

So the key to making all of these interactions really smooth is just making sure that the transitions between these states are as seamless as possible. And so let's start out by talking about transitioning from your gestures into your animations. So it's often the case that you're doing something like dragging content around using a PanGestureRecognizer.

And you're maybe having it track your finger directly because that's a really great way on a multi-touch device to optimize gesture interactions. But then when you lift, you might want to start some animations, something like what a scroll view does, to do a deceleration or something of that nature. And you want that transition point to be as smooth as you can. So let's take a look at two possible ways to do this.

I've got this on loop, so I'm just going to leave it up here for like 15 minutes and we can just stare at it for the rest of the day. The bottom one is looking a lot smoother than that top one. We've got two fingers that are dragging these boxes.

This one comes off when the finger lifts with the same velocity and it comes to rest nice and smoothly. The top one, as soon as I lift my finger, it snaps to a stop and then slowly starts up and then comes to rest. So we want to figure out a way that we can make all of our transitions from gestures to animations behave like that bottom box.

And the first part to making this happen is to figure out what velocity we want to start our animation at. So number one concern: compute velocity. Now, if you're using some of the standard UI gesture recognizers, this is really easy to do because it's actually already done for you. So if you're using a UIPanGestureRecognizer, you've got this velocity and view method and you can just pass in a view and you'll get back the velocity the panning was happening at and whatever coordinate space you need to start your animation.

So that's really easy. If you're using a pinch or a rotation gesture recognizer, you get the exact same kind of thing, just for whatever it is that you're computing. In the case of UIPinchGestureRecognizer, you're getting the velocity of the scale. And for the rotation you're getting angular velocity for that rotation.

So computing is really easy if you're using one of these standard gesture recognizers. If you're not, computing velocity is still a pretty straightforward thing to do. Velocity is just distance over time, so if you just keep accumulating your distance and your times and keeping track of that, you can compute the velocity that you should start yourself for whatever interaction you're interested in. So that part's pretty straightforward. The interesting part is, how do we start our animation at that velocity? So let's take a look at the standard UIKitAnimation method. We've got animateWithDuration, delay, options, animations, completion.

So there's no velocity in there. So that's not going to be obviously an easy way to go about doing this. But happily last year with iOS 7 we introduced a new, more expressive version of this method that had two new parameters in it. We've got usingSpringWithDamping and initialSpringVelocity. So that is looking a little more promising.

So let's focus on that initialSpringVelocity. This is the thing that is going to let us pass the velocity out of our gesture recognizer or wherever we computed it and start an animation that feels like it's coming off the finger really, really smoothly. So how do we compute this initialSpringVelocity? Do we just take that velocity that we had and pass it in? Obviously if the answer was yes, I wouldn't really be talking about this. So we're probably going to have to do something else.

So let's take a look at an example, assuming that we were panning some object and we want to start having it decelerate just along the x-axis for simplicity's sake, and see what we do. So we've got a box. We were panning that around. It's at point 50 right now, x-coordinate 50, and we're going to have it animate over to the right side there to x-coordinate 150.

Now, let's assume that we calculated that our horizontal velocity was about 50 points per second, so that's where we start this animation at. To figure out what velocity we want to pass in for the initialSpringVelocity, we actually have to compute a normalized velocity. The coordinate space that this method is expecting to get the velocity in is a normalized coordinate space. And we want to normalize it based on the total distance that this object is going to travel during the animation.

So first we have to calculate that distance. So we've got our destination value, that's 150. We want to subtract from that the starting point. So we subtract off 50. And we get a total distance traveled of 100 points. So then to figure out the velocity to pass in, we just normalize the velocity we want by dividing by that total distance. So we take our 50 points per second, divide by the total distance traveled of 100, and we get an initial velocity of .5.

That .5 we just passed in, then, to the method that we just took a look at as our initialSpringVelocity. So UIViewAnimateWithDuration, we can give it a time, how long it's going to take, delays, all those other things that you normally do, and just pass in that additional velocity parameter and we'll get a really nice effect.

So then if we take a look at what that's going to end up doing, we can see that if we drag this box around, we get it coming off our finger really smoothly. And, additionally, we get a nice bounce there at the end as it comes to rest. That bounce was the other parameter we saw and you can tweak that to get a different kind of feel, whatever feels good in your apps. You can get nice springy animations as they come to rest with a smooth velocity coming off your finger.

So that's what you can do with standard UIView animations. But there's other options, as well. So let's say maybe you wanted to do something a little more advanced and have more types of interactions with other views on screen or more complicated decelerations than just a spring. So also in iOS 7, we introduced a class called UIDynamicAnimator.

And this is another great way to go about getting animations that feel like they come off your finger at the exact velocity that you were interacting with. So let's look at how we'd set that up in a UIViewController in order to get some animations going with the DynamicAnimator.

So first off we would create a UIDynamicAnimator. Now I'm doing this in Swift, so there's a couple of interesting things we can talk about while we go through this. Here, I'm assuming this is a property on our view controller and I've typed it as a "UIDynamicAnimator?" And I actually just made that an equal sign. It should be a colon.

I literally changed this on the stage before I got up here because I thought it was wrong and now I just realized while I'm up here I changed it to the wrong thing. So don't write code in Keynote. It's a bad idea. I'm trying to declare a DynamicAnimator of type UIDynamicAnimator optional, an optional UIDynamicAnimator, because we're going to go and initialize this later.

The reason that this one needs to be optional here is because UIDynamicAnimator, when you create one, takes a UIView at creation time. And you don't have a UIView at creation time of UIViewController because you want view controllers to load their views lazily. So we want to start out with this being nil and then initialize it later.

So, anyway, we've declared our property for it. We'll go and set it up a little bit later. Now we've got our dynamicItemBehavior, which is a class that participates in this UIDynamic system and this is where we're going to attach views that we want to have dynamics associated with. So we create a UIDynamicItemBehavior and we can initialize this here as a constant because we don't need to provide any views at the time we create it. It lets us add or remove views over time, so we'll create it ahead of time, create our UIDynamicItemBehavior.

And now we'll override the viewDidLoad method because, when a view controller goes to load a view, it's going to load up all the view bits and then there's a good place to go and add additional initialization after the view's loaded in viewDidLoad. So we'll call super just to make sure that it gets a chance to do any work it was going to do.

And then we can go and set up the rest of our dynamic system. So we have that optional UIDynamicAnimator. This is a great place to initialize it because now we know we have a view. So we can create our UIDynamicAnimator. We have our view now, so we can pass it in.

We also want to set a couple of properties on the dynamic animator, just so that as we set velocities and get some deceleration animations, things will come to rest smoothly. So we're going to set our resistance and angular resistance to a value I just picked randomly of 3. You can pick different values and get different feels. And then, finally, we're going to add that behavior. We created the dynamicItemBehavior earlier. We're going to associate it now with the DynamicAnimator now that we have it.

And so that's pretty much all there is for setting up the system. So then we have to figure out how to transfer velocity into this dynamic system from our gestures because that's really what this is all about. So let's take a look at how we do that. The first thing we want to do is find out what view we're going to be associating this velocity in. So we've got the panGestureRecognizer. Let's assume that it's attached to some view that we're dragging around. We'll get the target view.

Now this is what you would often do in a UIGestureRecognizer target action method, so I'm assuming that we're doing that here. So let's do the normal switch statement that you would see within a target action method for a gesture recognizer. So we'll get the panGestureRecognizer state and switch on that. Now, right now we're just talking about transitioning from the gesture to an animation, so we won't worry about any state other than ".Ended" because that's when you perform this transition. So let's go and deal with the .Ended case.

So now we have to get the velocity that we want to move into the dynamic system, so we'll ask the panGestureRecognizer for the velocity. And the view usually that you'll be doing here, if we're dragging around the view it was attached to, we want to get the velocity in that view's super view because that's the coordinate space that the view's geometry is going to be in.

So we'll get the velocity in that targetView's superview's coordinate space. And then we'll just ask the dynamicItemBehavior to add some linear velocity. Adding linear velocity will just increase the velocity in the dynamic system of the object by whatever the current velocity was that we were dragging it around by.

So we can add that into the system and then it'll smoothly decelerate from whatever the gesture was doing. So, again, now if we do look at some boxes and view this, we can see that as we drag these things around they decelerate smoothly. But because this is dynamics, they can also interact with one another. So as they collide, they're going to start doing other interesting things. So this is a great way to get more advanced behaviors while still preserving these really smooth transitions.

So we've talked about two ways to go about doing this. We've got animateWithDuration. Now, there's different reasons why you might choose one or the other, and there's no one right answer for all situations. animateWithDuration, one of the nice properties of doing this is that it creates server-side animations.

Now, what that means is that the animations that are created using those spring velocities that we talked about at the beginning, those are actually going to execute in another process out of the context of your application. So even if you end up doing some work in your application that maybe blocks your main thread for a little while, the other render server is running at a higher priority.

So it's going to be optimized to make sure that we try not to drop frames on that animation, even if your app has a lot of heavy work going on in the main thread. So it becomes easier to maintain smooth animations if you just offload it onto the render server by using that animateWithDuration method. Now, of course, the other option we talked about is UIKitDynamics, so UIDynamicAnimator.

Now, one of the big benefits of this is that you get even more advanced interactions. And as we'll see a little bit later when we talk about more transitions, some of the transitions can be easier when you're using a UIDynamicAnimator than when you're using the server-side animations, because with UIDynamicAnimator everything is happening in your process, in your address space.

Now, the downside of that is it means it is going to rely on your main thread turning often enough that the DynamicAnimator can update the dynamic system and get everything committed for the next frame. So if your app happens to do a bunch of work or, you know, is taking more time to get something done, it's easier to drop frames if you're not being careful. So UIDynamicAnimator is a very powerful way to do it. You just have to be a little bit more careful to make sure that you're optimizing your app to avoid dropping any frames.

Now, there are other options that are a little bit more advanced. And, in fact, the thing that UIDynamicAnimator is built on is CADisplayLink. Now CADisplayLink is a class that calls you back once every frame. So a frame is going to get rendered, you get called back to go update your app in whatever way you want.

Now, that will give you 60 Hertz callbacks and so you can try and update your animations at 60 Hertz. That's exactly how DynamicAnimator does it. So if you have some really custom thing that you can't accomplish with one of these other technologies, you could build your own animations to do things yourself.

Now, of course, this is an even more advanced thing. We're not going to go into exactly how you would go about writing arbitrary animations. If you have an idea of something you might want to do, you probably can go build it yourself. But some of you that are out there may be thinking, "Well, wait, hang on. There's another option. Why aren't we talking about NSTimer?" This was a common thing before CADisplayLink existed. You just set NSTimer, set it to call back at 60 Hertz, and drive animations off that.

Well, to understand why we're not going to talk about that as a really viable option, let's take a look at a timeline showing what happens over time for NSTimer, CADisplayLink, and all of the display frames that are coming on screen. So here I've got some vertical bars. Each one of those represents a display frame.

Now let's take a look at what CADisplayLink does within each of those gaps. So because we're rendering at 60 Hertz on iOS, each gap there represents about 16 milliseconds. It's a little bit more than that, but that's roughly right. So that's how much time you have in between any two frames.

Now CADisplayLink, as I said, is going to call you back once per frame. And the place where it calls you back is going to try and be as tight as possible to where the frames are happening. So you're going to get these green dots; that's callback times that are associated roughly with where the frames happen.

So what happens with NSTimer? Well, it depends. NSTimer, you might get them there. Maybe you'll get them at the beginning. Maybe you'll get them closer to the end. But let's say you get them in the middle. You actually aren't really sure where you're going to get them because when you get them depends on when you happen to set up the NSTimer.

NSTimer isn't synchronized with the display in any way, so when you get the callbacks will depend exactly on when you set up the timer. If you set it up for a 60 Hertz interval, it'll be 16 milliseconds from whenever you set it up, depending on where in the frame that was. So it could be anywhere. It's hard to know.

Now, why is that a problem? Well, looking at how much time we have now, we had 16 milliseconds for this whole frame. With NSTimer there, we've got about 8 milliseconds from the time that it got called until the next frame, assuming that it happened to be set up right in the middle.

Now, even then, you might not see why exactly that's a problem. I mean, so it's eight milliseconds. You can probably get done in less than eight, or if you can't, then all right, you're always in the next frame. Well, the trouble comes in the places where maybe you aren't always either in the current frame or the next frame.

So let's say that it actually takes you about 8 milliseconds to set up your frame and get it committed to the render server. So if we look at our second frame here out of these that I have listed, let's say that the first time we get it done in eight milliseconds, but that happens to miss being rendered to the screen for this frame. So it goes over and it'll come in, you know, another 16 milliseconds later. So we missed that frame. Now the trouble is that maybe the next time we happen to get things done a little bit faster and do make that frame.

What that's effectively going to end up meaning is that the frame where we missed will really just never display to the user. It'll be as if it just disappeared. And that's going to end up looking like a dropped frame. So we're rendering within eight milliseconds. It's only taking us eight, and we have 16 to get things done, so it seems like we should never drop a frame because we're well under the total frame time.

But yet we just dropped a frame because we were right on that border. With CADisplayLink, if it was taking us eight milliseconds because we started close to the beginning, we still have that extra eight-millisecond buffer time every time, so there's no way we're going to miss that frame.

Now, if you do take 16 milliseconds, you might get back into that same situation, but if it's taking you 16 milliseconds to prepare a frame, you're basically never going to hit 60 Hertz anyway. So at that point you're back to, you have to optimize your app. But if you're finding that you're optimizing things and you're using an NSTimer and you've got it as fast as you can and you're still dropping frames, it's probably because you're using an NSTimer and just hitting this variability, and you should switch to using a CADisplayLink.

So NSTimer's never really the right solution for doing client-side animations. So that's the story for transitioning from a gesture to an animation. To give you an idea of now how to transition from animations to other animations, Andy's going to come up on stage and talk a little bit about that.

[ Applause ]

Thanks, Josh. So you've got an animation going and you want to change directions. Many of you have probably seen this issue before. You've got this purple circle. You say, "Go to the other side." And in the middle of its animation, you say, "Wait, no, go back the other way," because maybe the user pressed a button.

And then what happens is the circle jumps all the way to the right side and animates from there to the left. This is a pretty common bug that we see in applications, and it's one of the reasons why people will, for instance, disable user interaction on their animations, to get around issues like this. Now, there's always been a UIViewAnimation option that can help deal with these scenarios. It's called UIViewAnimationOption BeginFromCurrentState. And this is what it looks like by contrast. So there's no jump in position. And that's great.

But there is a jump in velocity. If you look at this animation, it's kind of like it hits a brick wall and then it starts going back the other direction. There's a more fluid option that I'm going to talk about now and I'm going to introduce you to it. It's called additive animations.

Take a look at how this looks. The circle on the bottom moves and it preserves both position and velocity when it changes direction. Both of those are smooth. Let's see that one more time. You can see BeginFromCurrentState, it's kind of like it hits a wall. Additive animations, the velocity component is smooth.

So absolute animations have been the default on iOS, so far, but starting now with iOS 8, additive animations are actually going to be the default behavior for all UIKit-created animations, [applause] most UIKit-created animations. We'll talk a little bit more later about exactly when this is going to work and when it's not. But the long and short of that is that, if you have this code, a typical animation thing, you are going to get that desired behavior.

So, I do want you to actually understand what's going on behind the scenes here. I think that's really important to taking advantage of this new feature. So let's walk through how Cocoa Touch actually manages animations in the system. What's happening behind the scenes to make this happen? So you've got this circle and you tell it, "Go to the other side." You say, "Animate that x position to 500." So if you were to ask the circle for its center's x value immediately after making that call, it would return 500.

So you think, "Okay, well, that means the circle should draw all the way on the right side," but it doesn't. The first frame that the circle is rendered is still going to be at 0 because it's going to animate from 0 to 500. And the reason that that happens is the distinction between presentation layers and - presentation layer's values and model layer's values.

So if you're unfamiliar with that distinction, I suggest watching the talk that Josh and I gave in 2011 called "Understand UIKit Rendering". I'm mostly going to assume that you understand that distinction already. The point is that where the layer's value is ,- I'm sorry. The layer's current value is not necessarily where it's going to draw.

There's a presentation value that's used for rendering. So right now the presentation value of this layer is 0. And the reason that it's 0 is that there is this CAAnimation object which UIKit has created and attached to the layer. It encapsulates a few pieces of information. You'll note that it has a fromValue set.

Now, fromValue was read out of the model value of the layer. The model value of the layer was (0,0), so the fromValue of this animation is (0,0). You'll note that there is no toValue and that's because the way this works is that at every frame, the toValue is going to be the current model value of the layer.

So if we were to read this animation in natural text we would say, "Okay, this is an animation that's going to animate from (0,0) to the current model value of the layer, which happens currently to be 500 over one second starting from 1000.1." So this animation emits a value of (0,0) for time 1000.1 and that's why the presentation value is (0,0).

The next frame we render, it emits a slightly greater time. That is the value that we use for the presentation layer and that's how we move across screen. Now, if we were to start reversing direction partway through this animation, say we set the center of the circle back to 0, then just as before, the model layer's value jumps immediately back to 0. But it's not going to draw at 0 immediately. So in terms of these CAAnimations, the next thing that happens is just going to be that we destroy the current CAAnimation.

And that's going to be why you see that jump. We create a new CAAnimation to replace it. And, just like before, we get that new CAAnimation's fromValue from the current layer's model value, which was 500 in this case. Now, we didn't make it all the way to the 500, but the model value was 500.

So this animation's fromValue is 500, and that means if we were to read this in natural text that we're going to be interpolating from 500 to 0 over a second. And indeed, the next frame we render, the animation emits a value of 500 and we go ahead and use that to render the circle's position. And that's why we have that jump. So we'll animate from 500 back to 0 across the screen until we finally get to 0.

BeginFromCurrentState is a little different, and I'd like you to understand how that works, as well. So say that we're at this point. We were partway through the animation and then we emit the code to reverse direction. So we say, "Animate again. This time begin from current state." Well, again, we're going to destroy the animation that was preexisting and create a new animation.

But this time, we're going to create a new animation with a fromValue derived from the presentation layer's value, rather than from the model layer's value. Now, at the time this animation was created, the presentation value was 150, and so we make an animation which is going to animate from 150 to 0 instead of from 500 to 0. And, therefore, the next frame that's rendered, the animation emits a value of 150 and so we don't have that positional jump.

So it seems like everything's good. We're animating back towards the origin. But we still had that kind of brick-wall feeling as we looked at these three circles earlier, and I'd like you to understand why that is now, as well. So say that we're going to be animating this circle from the left side of the screen to the right side of the screen and just taking snapshots at a regular interval as it goes. The snapshots might look something like this.

Now, if partway through that animation we were to stop and try reversing direction of the circle again, and then if we were to take snapshots at the same interval, we would get snapshots that looked a little bit like this. So the first thing we notice is that we are animating a smaller distance over the same amount of time. And that's going to create a problem because we can see that the slopes of these two lines are not the same. That's not going to be smooth.

But that's not the whole problem. You might think, "Okay, we could adjust the duration. We can get around this." Let's think about easing. If we're doing ease-in, ease-out, this might be what those snapshots looked like. And then if we were to stop partway through and reverse direction, that problem would be exacerbated. Looking at a plot, this is the situation we have.

Really, the issue we have is that the velocity component isn't smooth. There's that kink in the middle of the graph. And that kink is exactly what additive animations are going to help us solve. So I'd like you to understand how additive animations actually work under the hood. Returning to this example here, let's say we try to animate across the screen.

As before, we're going to create a new CAAnimation and we're going to have this additive flag set to YES. This is a property that already exists on CAAnimation. You can use it in previous versions of iOS. It's just that UIKit is configuring this for you now by default.

The fromValue is a little different. The toValue is now present, so let's talk about those. The fromValue and the toValue are interpreted relatively to the model value at every frame with an additive animation. That's basically the purpose of additive animations. So we create a fromValue by subtracting the previous value from the destination value, basically 2 minus from. And we create the toValue by just assigning it to 0.

So what that's going to do is at every frame, that animation is going to emit a contribution - here, -500 - which, when added to the model value, currently 500, will form the presentation value, 0. So 500 plus -500 is 0 and that's why the first frame is that circle all the way on the left.

And as it starts moving towards the right, the animation's contribution decreases. It's always getting closer to 0. So -500, -450, -400, and the sum is getting closer to 0. So now partway through, if we reverse direction, again, the model value jumps over to the left side. But this time we're not going to destroy that animation. We're going to make a second animation and add it on top.

So now we're going to use both of those animations' values, 0 plus -300 plus 500 is 200. And you'll see even though we've added this animation to reverse direction, we overshot a little bit. We didn't reverse direction instantly, and that's because we can't do that without making a jump in velocity component. So here we overshot a little bit, but now the second animation's going to overtake the first animation all the way until we get back to the origin. So they stack.

[ Applause ]

I'm glad you like it. So we get back to the origin, they both have a 0 value, everything's happy. We had this graph, and now we have this graph. [applause] All right, excellent. So I promised that I would tell you a little more detail about when this is going to work and when it's not going to work. So there are a set of keys on which this behavior is supported. Basically they're your geometric keys.

So this kind of stuff is all going to work fine for moving objects around on the screen, scaling them, rotating them; that's all going to be fine. There are certain kinds of rotations, though, which work and ones which don't. So, just really briefly, if you were to actually try to animate the layer's transform property, you're only going to get this additive behavior for affine transformations. The definition of an affine transformation is that it keeps parallel lines smooth after the transformation's applied. So the bird on the left, we've rotated it about the z-axis. Those parallel lines - I'm sorry, parallel lines are not smooth. They're still parallel.

Parallel lines pre-transformation remain parallel after the transformation. That is an affine transform. So the bird on the left, parallel lines still parallel; bird on the right, parallel lines no longer parallel. So a rotation animation that is like the bird on the left will use additive animations; and rotation animations, like the bird on the right, will not.

There's a few other compatibility requirements, as well. This feature will not work with keyframe animations at the time being and it also will not work if there are pre-existing repeating animations because that could get you to a place where you have an unlimited number of animations stacking forever. It's not good for the system. Finally, it will also not work if you've gone behind UIKit's back and created preexisting absolute animations on that layer. But these are, perhaps, not common situations.

Now, I said that not all keys are supported. For instance, alpha is not supported. And you don't really know what setting the circle's tintColor is going to do. You know that that's an animatable property, but you don't really know what that means. And so you can be defensive about whether properties support additive animations or not by continuing to use BeginFromCurrentState.

So for center.x, for that animation, we're not going to use the old BeginFromCurrentState behavior behind the scenes. We are still going to make that additive animation. But for the other two properties, for alpha and for the tint color, then at least you'll get position preserving transitions. So you can go ahead and use BeginFromCurrentState, center.x will be additive, the others will use the presentation value in their reverse directions. The other thing that's interesting about this transition is sort of a side effect. You know, UIKit doesn't actually have a way for you to cancel in-flight animations right now. There's no way to do that. So some people do it like this.

They make a new animation with a 0 duration, and you just set the value to something else. Now, that will sort of stop in-flight animations just because the behavior of UIKitAnimations has always been that, when you make a new animation, we throw out the old one, replace it with the new one, and this new one's going to be 0 seconds long. But, as we just saw, we don't throw out those animations anymore. So this is not going to stop any in-flight animations. Instead you just have to drop down to the CAAnimation API directly to do your work.

One other thing to keep in mind is behavior completion handlers. These animations stack. So before, if you had an animation with a completion handler, you started reversing direction. As I said, we were going to remove that animation, so when we remove that animation, we call your completion handler. The argument is finished, and so when we call your completion handler, we call it with a value of false, because the animation didn't finish. And then we'll create the new animation.

And when that one completes, we'll call that completion handler. Well, in the additive animations world, if you have an additive animation with completion handler and then you reverse directions, then - remember, we don't throw out that animation. We make a new animation. And so we don't have a completion handler to call right now. The animation's still going.

It's only when we actually get to the end of the animation that we call the completion handler, and we call the completion handler with a finished argument of true, because that first animation did finish. It's just that another animation was added on top. So this could be surprising, potentially, and what I'm telling you is, do not be surprised.

[laughter] So, in summary, transitions will be smoother by default. This is a great thing. However, there are some kinds of animations that are not yet supported with this system and so you should still use BeginFromCurrentState if you're not sure about whether you're animating a supported key, a supported scenario or not. That's fine. We'll do the best thing we can do.

And, finally, do keep in mind that those completion handlers may stack because that does actually represent a change in when we are calling you back. So we've talked about how to transition from animation to animation. And now that you've got these animations in-flight, you do still want to be able to interact with your application. So Josh is going to come back up and talk about transitioning from animations to gestures.

[ Applause ]

All right. Thanks, Andy. We're getting close. We've got one transition point left that we're still finding we've got some issues with. And by default probably you're noticing that in this transition point, going from your animation back to your gesture, you're actually by default not actually able to even start an interaction. So before we can even talk about making it smooth, we have to talk about how to allow gestures and touch events and interactions to begin while you're in the middle of one of these animations.

So let's go and take a look at why this is the case and what we can do about it. So here we've got some big view and I've put a subview in that I've colored blue just to make it stand out. And we're going to say that we're taking that view and we're animating it over to the right into this position on the right side of the screen with some standard UIKitAnimation using the UIViewAnimateWithDuration API. And let's say it's halfway through. And halfway through, we want to start interacting with it.

So let's say that a touch comes down on the screen and it comes down on top of where that is visually in the presentation layer. Now the default thing that you get if you just do UIViewAnimateWithDuration is that animations that are in progress as a result of that do not allow user interaction during the animation.

So that view is going to get hit tested and the touch will be attached to that view, but because the interaction is disabled by default, what you'll find is that the touch actually doesn't get delivered anywhere and just gets swallowed. It just disappears. It's as if it didn't even happen.

So obviously we want to fix that, and the way that we can go about doing it is by using an animation option called UIView animation option .AllowsUserInteraction. Not too surprising, the name. And you can pass this into the UIViewAnimation AnimateWithDuration APIs. And this will enable interaction during these animations.

Now, once you've opted in to allowing user interaction, you're basically saying, "I've got this. I'm an advanced developer here and I know what I'm doing and I'm going to take care of making sure that the right thing happens." So, you know, by default, UIKit is making it safe so that you don't have weird things going on. But once you opt in, you're saying that you're going to deal with it and you know how to do the right thing. So let's talk about what the right thing is.

So, again, let's look at what happens if a touch comes down in that view. Now, this part is probably going to be a little surprising at first if you've tried it. What actually happens is that you'll find that the touch gets hit tested to that big view in the back, which probably wasn't what we meant because we touch visually on what was there.

In fact, if you go and put a touch down where we're moving the additive view to, where it's going to animate to, you'll find the thing that we hit test is the thing that is currently animating over to there. Understanding why that happens goes back to what Andy was saying about the difference between model and presentation values.

Once you've enabled user interaction, you're saying that you're going to take control of this whole system and you're going to decide the right place to go do a hit test. Because we don't actually know if the thing that you're animating is something that you intend to interact with or just something that is animating in a system, but you're actually trying to interact with the thing behind it. So really we're hit testing the model values of all these layers, but if you want to interact with the thing that's animating, you really want to be hit testing the presentation values.

So we have to go talk about how we can make a presentation layer hit test, instead of a default model value hit test. And to do that we can override the hitTest withEvent method on our UIView that's animating to make it so that it's going to hit test its presentation value instead of its model value.

Now, by default, the method is going to take a point. It's the point in the view that is being hit tested and that's the view, or the point that UIKit thinks you have touched. But, as I mentioned, it's done that in model space. So we have to convert it back to presentation layer space to see if it's currently inside where we are as we're animating.

So we can take that point that's passed in and convert it back into our superview. So we'll convertPoint toView or superview. Once we've got it into superview's coordinate space, then we can convert it back into our presentation layer space. So we convert out using model space and then back in using presentation space so that we can see where it is inside of where we are currently, given that we have an animation in progress.

So we can do that by getting our presentation layer and using that to do the conversion back into our coordinate space. Once we've got that converted into the right space, then we can just call super and let it do what it was going to do. So return super.hitTest, passing in the converted point now in presentation space and everything else can just move on as it had before. So once we've done that, we can go take a look at what was going on again. Now we've enabled these interactions, we touch down, and now the right view is going to get hit giving what we were trying to accomplish.

Now, keep in mind that other APIs - like touch, location and view - are also assuming model space conversions. All the UIKit behaviors by default are converting using model space. So if you've now enabled hit testing into something that's animating and you're in the progress of doing that animation and you want to start handling touches in the view, if you ask for that touch location in view, it's going to be back in model space. So you would have to be careful to go and make sure that you know which space you're interacting in.

So really, the reason that the default is disabled is because, once you enable interactions, you have to start knowing more about what's going on and being careful to be using the correct coordinate spaces. But most of the time, this is actually probably not going to be a problem, because usually if you're enabling under interaction on something that's animating, you're probably trying to grab the thing and stop the animation.

So really what we're going to talk about now is how we stop the animation. That's the key part that we're interested in right now. We've got hit testing working. Once we're touching down, how do we stop an animation? Now, as Andy mentioned, there is no direct API on UIKit to stop an animation.

So we're going to use the CALayer API to do that. But there's another interesting component that we have to keep in mind, because Andy just ran you through the whole story of how things go when there are animations on a layer. And part of that is that the model value is already at its final location.

So if we just removed the animation from the layer, that was the thing that was keeping it in the center of the screen visually right now, so it would snap to the end. So we would touch down on the object, remove the animation, and it would snap out from under our finger and land wherever it was going. So we don't want that. We have to go get the presentation values of where it currently is on screen and set those to be the new model values so that there's no jump.

Now let's assume the thing we were doing was animating just its position, its location on screen. I'll give you an example of how that would look in that case. Of course, if you're animating something else, like its bounds or some other property, you'd have to do the same thing for whatever property you're animating.

So in this case we're just doing the center, so let's get the view, get its layer, get the layer's presentation layer, which is going to be where it is on screen right now. And in this case, I'm asking for its position because that's where - its location on screen. So we'll store that away here.

Then we just want to set that to be the new model value so that once we remove the animation it doesn't jump. It's going to end up right here. Now, presentation - I'm sorry, UIView's property for this is called center. CALayer's is called position. They're actually the same thing, so you can just assign one to the other and not really worry about it. They are the same property. They just have a different name in UIKit and Core Animation.

So once we've set that, now the model value is putting it where we thought it would be visually, because that's the presentation value. So we can just go and remove the animation. Now, in this case I'm taking the shortcut and just removing all animations. Now, you probably want to be a little more careful than that when you're doing it yourself to make sure you're removing only the animations you really mean to be removing. You have to be cognizant of whether or not there are others there. But that's the key to it, is just removing the right animations. So I mentioned that things were going to be a little easier when we were talking about UIDynamicAnimator earlier in the talk.

Now, this is the place where things get easier when you're using a dynamic animator. Because DynamicAnimator doesn't have presentation and model space, we don't have this same complication of having to figure out where it is on screen compared to where the model is or anything like that. DynamicAnimator, it's all just wherever it is and it's happening in your process and the model value is the correct position on screen right now.

So stopping this for a DynamicAnimator is a lot simpler. So let's take a look at that code that we wrote earlier in our GestureRecognizer method where we were handling the .Ended case. Now, of course, we're trying to deal with new touches coming down so we want to handle the .Began case on our Gesture Recognizer. So we'll make some space for that.

And my suggestion for what you would do here - there's a couple of things you could do, but the one I'm going to suggest as a good option - is to actually just remove the thing that was animating from the dynamic item behavior, because once you've removed it, it's no longer going to interact with any of the other views; it's going to lose any velocity it may have had, because you just took it out of the dynamic system; and now you'll be able to manipulate it and move it around yourself. So we can take the dynamicItemBehavior and remove the thing that we're trying to interact with, our target view.

Now, of course, once we've removed it, it's no longer going to have any new velocity when we try to add velocity later, so we have to put it back in when the gesture's done. So we'll take that dynamicItemBehavior and add it back at the end. And there's other things you could do instead.

If you really wanted to leave it in the dynamic system, you could subtract off the current velocity so it would just come to a rest. But, first of all, the easiest thing to do is to remove it, and it's also probably what you mean in a lot of cases, because it'll stop it from smacking against other views in your system as you drag it around.

So now we've really dealt with most of the transitions that we wanted to talk about. We got our gesture to animation going, we got animation to animation going, we got animation back to gestures working. So it seems like everything's good. We all - visually, everything looks great. So we should be done, but we've got 15 minutes left, so what's going on? Well, there's some complications that really start to come into things once you do this in a real application.

So far everything we've been talking about is pretty theoretical. If you're just doing animations, it's all pretty easy. But if you - in any real application you have other state, transient animation state, that exists only during transitions or animations. Maybe when you start some transition, you create a bunch of extra views to use during that animation, and then when the animation is done, you destroy them. You have to know what the right times are to create and destroy these things.

And if we're stacking animations with additive animations and we're figuring out that we're starting and removing things in the middle by allowing interaction with gestures, it's no longer immediately obvious where the right places are to make some of these changes to your view hierarchy to set up and tear down your transient animation state.

So we're not going to go through too much in slides about this actually. What we're going to do is just do a demo showing you how you can make this happen in a real app. So Andy's going to come back up and write an app for you that does all of that.

Thank you very much. I am extremely excited about this part of the talk. I don't think we've ever talked about this topic publicly and we've been confronting it a great deal ourselves as we think about how to effectively deal with these kinds of problems. We want everything to be fluid as much as possible in our system, and that means wrestling with these problems. There's a superstructure of state around your animations which you need to deal with, which you can't forget. So I have a very important application here. It's been a key thing for us.

We're using it as a test ground for this experimentation, and it's called Toggle Bird. Now, sometimes you need a bird. So, great features, you press a button and get a bird. Sometimes you don't want a bird. You press a button and the bird goes away. Now you don't have a bird anymore. That's great.

But, sometimes you're indecisive. You say, "Okay, well, does this bird - no, I don't actually want the bird. I was overeager with the bird. Put the bird away. Go away, bird." But you can't, because the button's disabled. And that's frustrating. You don't really want the feeling where an animation's in progress and so the world must stop until the animation is done.

So we could look at our code and say, "Oh, okay, well, I see a couple of pretty big red flags here. Looks like we're disabling and re-enabling the birdToggle button when this animation's in flight, so I'll just go ahead and comment out that code and then everything will be cool. You know, the button's not disabled anymore, looks good." Let's walk through this method together because I think it actually illustrates a lot of the common problems that people have when trying to make this kind of thing work.

So this is just the setter for the birdExpanded property, which you'll see is just a property. And this is a property which is animatable, which means I've designed it so that, if you set this property inside an animation block, then it'll be animated. I think that's a nice pattern.

We do that by inheriting the parent's animation context by making a UIView animation block that is a zero-length duration. So you'll see here there's a toggleBird method that sets the birdExpanded property inside an animation block. That's how that part works. So let's walk through the rest of it together.

There's a bit of state that we keep track of. Is the bird expanded? And if the bird is supposed to be expanded anew, we make a birdDrawerView, set it up. We want it to animate from, you know, being full-width but zero height, so we do a little layout initially. So this is kind of like a preamble.

You know, we're making some stuff that we need, get it set up, and then we actually do the animation. This is the part that is now additive, so that's great. You know, we're just changing this constraint's constant, telling it to layout inside of an animation block. That will synthesize the correct bounds and position animations to actually get things moving. And finally there's something of a post-amble when that animation's completed.

If the bird is no longer supposed to be expanded, we clean this up because the bird is being drawn with some very novel and expensive rendering technologies, and we don't want to waste system resources on that. You know, you've got to be good Samaritans. So this is the entirety of what's going on here, and the problem arises when we remove these enabled setters because we can run through this method multiple times while the animation's in flight.

This completion block, as I said earlier, is going to run even if there are new animations related to the bird also running. So we really have a more complex state machine going on than it seems at first. So I'm just going to jump right to the solution here. Only a few lines change.

And first let me prove that I have solved our bird toggling problems. It's very nice. We can be indecisive about the bird safely. And here in the code I've only changed a few lines. I've got a new i-bar, and that i-bar is going to be tracking how many active transitions we have.

So at the bottom, every time the birdExpanded property changes, we increment that; and every time a bird expansion or contraction animation completes, we decrement it. So we're just keeping track of the number of in-flight state transitions currently. And then we change our preamble and post-amble to consider that counter. We say, "Okay, well, only do this preamble if we're transitioning from not having any animations to having some animations. Similarly, only do our post-amble if we're transitioning from having animations to no longer having any animations at all." And that alone solves this problem.

I do want to show you what this would look like with iOS 7-style animations. Note that the bird jumps to the finished value when I try to make it reverse direction. So even if I were to use the BeginFromCurrentState option to try to use presentation values. You see that when I reverse direction, it's kind of like the bird hits a brick wall. Now, we much prefer the additive animation solution. It's much smoother.

So all that's great, and I want to show you quickly, I'm just going to comment this a little more in order to make clear how general this structure I've created is. Almost everything I've written here doesn't have to do with birds or toggling. That is the key point, because you might be writing a frog toggler.

You know, I don't know; I want you to be creative. So there's a few sections here. There's a preamble section. In the preamble section, you can deal with all of the possible states. We only have a preamble for the bird becoming expanded. You could have a preamble the other way. Similarly, this counter, that's not about birds or toggling. That's just about transitioning between states.

And in the post-amble, we don't have a post-amble for the bird being expanded. We could. We have a post-amble for the bird being contracted. I want to point out that we're checking the state at the time of the completion handler being called rather than the argument being passed into this method because, again, these completion handlers can stack and the state might no longer be what you thought it was.

The one last thing I want to point out is that this is the solution for two states, but if you have three states that you could move between, you could handle that this way, too. You just need a counter for each of the states, and that counter would track how many transitions to or from that state or in-flight. Right now we only have one counter, because the number of transitions to or from each of the states in our bi-stable system is always the same. So this is a solution which generalizes and which doesn't have anything to do with birds or toggling.

  • All right, let's head back to slides to wrap up.
  • Thank you.

[ Applause ]

As I said earlier, we don't want to stop the world while animations are in-flight. They might be beautiful, but your user is trying to do things. We also want to keep things fluid. So, use that gesture velocity and translate it into the animated actions which result and makes an interface which feels more responsive.

Once you've got animations in flight, make sure that they smoothly transition to any new animations which might replace those first sets of animations. Finally, as I was saying a moment ago, don't stop the world. Try to make animations interruptible as much as possible. It is more work, but we've talked through some techniques for dealing with it.

And, finally, keep in mind that there may be additional associated states on top of these animations. That is often what makes it more difficult to enable user interaction while animations are in flight. But we've just talked through one technique which I think is quite general for dealing with that.

And finally, if you have more questions, I encourage you to ask Jake. We have a Core Animation Programming Guide which actually does talk about many of these topics and there's a developer forum where you can ask your peers questions. I hope you've had a fantastic conference and I can't wait to see what you make. Thank you.

[ Applause ]