Interactive Animated Transitions on iOS
I closed my talk at UIKonf by talking about a technique to build better animated transitions on iOS by making them “always interactive”. The user should be able to interrupt an animated transition with a gesture, cancelling it any time they want. By default animated transitions will wrap the entire transition in
[UIApplication beginIgnoringInteractionEvents], effectively blocking all interaction during the duration of the transition. It’s treated almost like a rotation event, which exhibits the same blocking behavior.
It should work like this:
The technique I’ve been using is to just turn an animated transition into an interactive transition.
Interactive transitions are supported by the transition system, it’s just that they usually start with a continuous gesture, like a pinch or a screen edge pan. That gesture kicks off the transition (be it a navigation controller push/pop or a modal present/dismiss), and then once the gesture ends, the relevant views are animated into place (or off screen, whichever makes sense for that specific transition).
And since interactive transitions don’t block interaction (of course), then we should be able to use them to make always interactive animated transitions.
The only difference is that the interactive transition starts with an animation, not a gesture.
At a high level, the difference between an normal interactive transition and an interactive animated transition looks like this:
Of course it’s a bit more complicated than the above illustration, and I touched on some of that complexity in my talk. The rest of this post will be a more detailed examination of how to implement these strange hybrid transitions.
Converting the Animated Transition
If you already have an animated transition, you’ve had to implement these two methods of the transitioning delegate:
1 2 3 4 5 6 7
The object you return from these methods conform to the
UIViewControllerAnimatedTransitioning protocol, and are responsible for defining exactly how an animated transition works by implementing the
animateTransition: methods. The transition system will call these methods after you’ve kicked off a transition, and you can return
nil to tell the system to just use it’s (the transition system’s) default transition.
To convert an animated transition into an interactive one you will also need to implement the “interaction” equivalent of these methods:
1 2 3 4 5
If these methods aren’t implemented (or you return
nil) then the transition system falls back to the animated transition and blocks interaction during the transition, which we want to avoid.
To turn on interactivity then all we need to do is implement these methods and return an object that conforms to the
UIViewControllerInteractiveTransitioning protocol, which only has one required method:
So let’s say we already have an animated transition that looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
And our transitioning delegate looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
Notice how we implement both “sides” of the modal transition by using that
reversed property. This is a fairly common technique since it’s a good practice to have symmetrical transitions (where each side of the transition is a mirror of the other).
To make it an interactive transition all we have to do is make this class conform to the
UIViewControllerInteractiveTransitioning protocol and move our animation code from
startInteractiveTransition:, like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
The only other change in the
startInteractiveTransition: method is the call to
finishInteractiveTransition in the completion of the animation.
Then, in the transitioning delegate, we can return the
animator object passed into the interactive delegate methods (casting it to tell the compiler that this object conforms to the required
1 2 3 4 5 6 7 8 9 10 11
At this point we have a working transition with an animation, but instead of blocking all interaction, keeps the door open for a gesture to take control of the transition at any time. Before adding a gesture recognizer to handle that case, we first need to fix a bug that will occur if we were to try and use this code in a navigation controller transition. You can see that bug in action here:
The navbar crossfade animation is delayed until after the animation finishes, instead of happening at the same time. Compare to how it should look:
Fixing the navbar bug
The transition system tries its hardest to animate the navbar along with a custom transition. With an animated transition, it knows the duration of the animation and so just tells the navbar animate your crossfade using a duration of X seconds. But with an interactive transition, there is no duration (the user could pan for an arbitrary amount of time).
If there is no duration, then how could the navbar animation possibly work? Interactive transitions have a concept known as completion percentage. This is a number between 0.0 and 1.0 that is supposed to signify how far along the interactive transition is in it’s own conception of “far along”. For example, if the interactive transition was driven be a pan gesture, from left to right, then the further to the right the user moves their pan, the further along the transition is and the closer to 1.0 it’s completion percentage becomes.
The transition’s completion percentage is updated by calling the
updateInteractiveTransition: method on the context, passing in a value between
The transition can use this completion percentage value to manually drive the navbar animation by using features of the
CAMediaTiming protocol. David Rönnqvist has a great writeup on implementing this technique in his Controlling Animation Timing post.
The transition system sets the navbar layer’s
0, effectively pausing it’s animation, and then uses the completion percentage to set the
timeOffset of the layer to “scrub” through the animation.
Knowing that we need to update the completion percentage to fix the navbar bug, how do we go about doing that for our interactive animated transition, especially during the initial animation?
The best way I’ve found to do this is to use a
CADisplayLink, which is like an
NSTimer but synchronized to the refresh rate of the display. This class is usually used to drive a custom animation system (see this objc.io article for an example) but we are instead going to use it to calculate the completion percentage during the initial animation based on the animation’s duration, using this formula:
percentComplete = elapsedTime / animationDuration;
This should run only during the initial animation so we’ll setup the display link right before the animation code, like so:
1 2 3 4
After adding it to the
mainRunLoop, the display link will start calling the
tick: method 60 times per second, but before that we record the
CACurrentMediaTime() which we’ll use to calculate the
tick: method looks like this:
1 2 3 4 5 6 7 8 9
Using the display link’s current
timestamp and that
startingTime we stashed away we can calculate the elapsed time (in seconds) and then calculate the completion percentage (capping it at
When the animation finishes, the display link needs to be stopped (so it doesn’t continue calling
tick: until the end of time) so we need to invalidate it, like so:
1 2 3 4 5 6 7 8 9
We’ve built the foundation for an interactive animated transition so now we can add the actual interaction bits. For this example, I’m going to cover how to use a pan gesture recognizer to create this interaction effect:
During the presentation or dismissal of the modal view controller the user can interrupt the transition by panning inside the modal view, deciding either to finish or cancel the transition.
If you’ve ever tried to add interaction to a view that is animating you know that it doesn’t work the same as a view at rest. That’s because of the Model vs Presentation problem. A lot has been written about this problem and how to deal with it (including the aforementioned article on interactive animations in objc.io).
The gist of the problem is that during an animation, the values of the properties being animated aren’t their current values as shown on screen, but instead are the values those properties will become once the animation reaches completion. An illustration of the problem:
So the view only ever thinks it’s in either the beginning location (before the animation has begun) or the ending location (once the animation finishes), but never any point inbetween while the animation is running. This is why a hit test on a view in the middle of an animation doesn’t work as you’d expect.
Fortunately, you can get access to a view’s presentation properties, which does reflect the property values as they are currently seen on screen, using the
presentation property on the view’s layer:
To add our pan gesture recognizer and have it only recognize when the pan happens inside of our modal view, we need to do a little extra work. First, we create it and add it to the transition’s container view, setting our
CustomAnimatedTransition object as the target:
1 2 3 4 5 6
This will match any pan inside the container view, so we have our
CustomAnimatedTransition object also become the delegate of the gesture and implement the method
- (BOOL)gestureRecognizerShouldBegin:, like this:
1 2 3 4 5 6 7 8 9 10 11 12 13
This method gives us the ability to decide whether a the gesture should begin before that recognizer’s handler method is called. We use the location of the touch in the container view to do a hit test on the view’s presentation layer, indicating that the gesture system should proceed if the touch is on the presentation layer.
Note Not shown above are the definitions of the two properties
contextis the parameter passed into
startInteractiveTransition:saved to a
weakproperty for use in the gesture recognizer delegate and target/action methods.
viewis either the “from” or “to” view, depending on whether this is a pop/push, or presenting/dismiss. In either case,
viewalways belongs to the view controller on top of the transition. For example, in a dismiss transition,
viewwould be the “from” view controller’s view.
Before we move on to implementing the gesture handler we need one more tweak to our existing code to get it to work. We need to ensure that our animation allows user interaction by including the
UIViewAnimationOptionAllowUserInteraction option, like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
Notice also that we remove the gesture once the animation has finished.
The Gesture Begins
Time to implement the gesture handler
didPan: method. Since we are working with a continuous gesture,
didPan: will get called multiple times and we will be performing different actions depending on the gesture’s state. Our first job is to handle the initial state,
UIGestureRecognizerStateBegan and cancel all animations, allowing the user’s gesture to take over control of the transition and the position of the view.
1 2 3 4 5 6
As soon as the animations are removed from the presented/dismisses/pushed/popped view like the code above, then the view will jump to it’s final location, not merely stopping in it’s tracks in the current location. This is because of that Model vs Presentation problem I described above: once those animations are no longer driving the presentation, the view assumes the position of it’s model layer, which as we now know is the view’s final position1.
So to prevent the jumping behavior, we again have to use the presentation layer. This time we use the position of the presentation layer to set the position2 of the model layer before removing the animations:
1 2 3 4 5 6 7 8 9 10 11
Note We’re stashing off the view’s center CGPoint to use later as the gesture changes
Have you ever wondered what that
BOOL finished parameter in animation completion blocks was useful for? Well wonder no more because there is finally a purpose for it! When animations are removed using
removeAllAnimations, the animation completion block will be called and the
finished param will be set to
NO. We need to use this to conditionally complete the transition or our gesture and transition will be short-circuited as soon as we remove the animations:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
We can invalidate the display link either way because it was only for driving the completion percentage while the initial animation was running.
The Gesture Changes
We want the view to follow the users touch as they pan around the screen. As the location of the pan gesture changes the
didPan: method will be called and the gesture’s state will be
UIGestureRecognizerStateChanged. We’ll use a fairly common technique to achieve this effect:
1 2 3 4 5 6 7 8 9
translationInView: returns a point that represents how much the location of the pan has changed since the start of the gesture, so all we have to do is combine the
initialViewCenter we stashed away at the beginning of the gesture with the translation to set the view’s new center position.
It might be tempting to save yourself from creating the
initialViewCenter property and just resetting the translation back to
CGPointZero after every change, but don’t do it. Resetting the translation to zero will muck with the finishing velocity of the gesture, which we’ll need to create a smooth continous finishing animation. As the docs for
setTranslation:inView: pretty clearly state: Changing the translation value resets the velocity of the pan
The display link has been invalidated but we still need to update the completion percentage as the user pans around the screen, based on the location of the touch. As the location moves along the y axis we’ll update the percentage based on the distance from it’s starting and final locations, like so:
So we need to do this calculate for every change in the gesture. Something like this will do:
1 2 3 4 5 6
The Gesture Ends
Now might be a good time to make a fresh cup of coffee. Check Twitter. Play some Threes. Because even though we have come to the end of our pan gesture, we’ve still got a lot of work to do. We’ve got to:
- Decide whether the transition should finish or be cancelled
- Create a smooth continous-from-the-gesture animation
- Complete the transition as soon as the view is offscreen
- Deal with both the forward and reverse cases.
Let’s get started :)
Deciding to finish or cancel
Before the gesture ends, the transition is in limbo. The user can decide either to finish the transition, or to cancel it. So we have to glean the meaning of the gesture that hopefully corresponds to the users assumptions about how it should work. Most tutorials I’ve read on interactive transitions (even the ones I’ve made) just use the location of the gesture to make the decision. Something like this:
1 2 3 4 5 6 7 8 9 10 11
They pick a reasonable threshold value (
160) and depending on the which side the location falls on. This is the naive approach. Instead, we should consider first the velocity of the gesture. If the user is swiping up, but lifts their touch in the middle of the screen, the view should continue along it’s velocity and animate up off screen. Subsequently, if the user’s finger is towards the top of the screen, but swiping down, when they let go the view it should continue towards the center. If we’d have just used location then the opposite would have happened in both cases.
If the velocity of the gesture is too low, indicating the user isn’t swiping much in any direction, then we can fallback on the location to make our decision. That could look something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
Let’s take this piece-by-piece:
If the absolute value of the velocity in the y direction is more than some reasonable threshold (I picked
50), then velocity is enough to decide whether the transition should finish or cancel.
y velocity is greater than 0 (which indicates the touches are traveling down the screen since the coordinate system starts in the top left) then the transition should finish.
1 2 3 4 5
If the velocity isn’t large enough to decide the outcome, then fallback on the location. If the
y location is over some threshold, then the transition should finish.
If the transition is running in reverse, then simply reverse the decision.
Now that we’ve made our decision, we have to let the transition system know that we’re either finishing or cancelling the transition, and we can do that like so:
1 2 3 4 5
This doesn’t do much for a modal transition, but for a navigation transition you want to make sure you remember to call these methods because the transition system will proceed to either finish the nav bar animation or reverse it.
Creating a smooth final animation
It’s time to animate the view to it’s final position. First we need to determine where to animate the view to, either back off the top of the screen or to the center:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
Then we can perform the final animation, like so:
1 2 3 4 5 6 7
Even though at this point we have a working interactive animated transition, something doesn’t exactly feel right with that finishing animation. Take a look:
The animation isn’t continuing from the velocity of the gesture, creating a feeling like we are pushing the view through goop. Unfortunately, animations driven by Core Animation aren’t able to take initial velocity into consideration, but we have some options:
- UIKit Dynamics
- Facebook POP
- Custom Animations using
Even though Facebook’s POP was built with this exact kind of use-case in mind, I actually rather like using UIKit Dynamics for this sort of thing. In their tech talk introducing POP to the world UIKit Dynamics is dismissed because it’s “hard to use it in a one off situation” and you’d have to change all your apps animations to use dynamics instead. I don’t think that is the case. UIKit Dynamics can very easily be used in one-off situations like this, for when you need to “animate” a view following a gesture.
So that’s what we are going to do, and by borrowing the behavior code from the Interactive Animations article we can build this pretty quickly. The only thing we need to change is to increase the dynamic item behavior’s resistance and the attachment behavior’s damping a little bit, like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
We’ll setup the dynamic animator back in the
startInteractiveTransition: method and use the container view as it’s reference:
Then back over in the gesture handler, we need to create the
PaneBehavior when the gesture ends (I renamed the class
1 2 3 4 5 6
Before we add the behavior to the animator (thus kicking off the dynamic system) we can assign an action block to the behavior which the animator will call on every “tick” of the system, and use that block to figure out if the view leaves the container view. When the view does leave the container view, we can stop the dynamic system by removing all the behaviors from the animator (this takes care of Step 3 from above):
1 2 3 4 5 6 7
When all the behaviors are removed from the animator, or the view settles into place in the center of the screen, the
dynamicAnimatorDidPause: delegate method will be called (remember we set the delegate on the animator to
self when we created it). This is where we’ll be cleaning up our transition:
1 2 3 4 5 6 7 8 9 10
That’s it! Phew. Grab all the code from this gist.
The things I’d really like you to takeaway from this post is to push the boundaries of animated transitions by making all transitions interactive, even if they start with an animation.
We are entering into a new world of Transitional Interfaces and so even though this may seem like a lot of work for little benefit, to me it’s worth it. And it’s not that much code, the entire example project weighs in at just over 400 lines. I’ve put it in a gist if you’d like to kick the tires.
If you have any questions or feedback (or a better way to do this) please contact me on twitter.
positionhere because that is what we are animating. It’d work the same way if we were animating any other animatable property, such as
Again, if you are animating other animatable properties other than
position, you’d have to copy those from the presentation layer to the model layer too.↩