Interactive animations with UIViewPropertyAnimator
Let’s dive into what UIViewPropertyAnimator is and why you should use it. Before iOS10, UIView’s animateWithDuration:animations: was the weapon of choice for view-based animations. Since user interfaces started to become more interactive, UIView-based animation lost their dominance. Frameworks such as Facebook’s POP came into the spotlight. When Apple released iOS10 in September 2016, they brought a brand new animation API to the table.
UIViewPropertyAnimator has a lot of contrasts to UIView animations. One wonderful characteristic of it is that it allows you to store a reference to it. This means animations can live on and change properties after they start running.
Further, you can not only pause and resume animations, you can also reverse and scrub them. This opens up a world of view-based interactive animations. Let’s have a look at an example.
Take Youtube’s floating video player for example. The player can both float in the bottom right corner as well as cover the screen. The user can transform the player by panning it up and down.
This animation is a great use case for UIViewPropertyAnimator, so let’s give it a go:
The view supports two states — playing in fullscreen mode and playing in thumbnail mode. Before diving into how it works, we must understand the different states of an animator object. Note that these are different from the fullscreen-vs-thumbnail state we talked about.
An animator is always in of the following states:
- Inactive: This is the initial state. As an animation finishes, this will also be the state it returns to.
- Active: The animator becomes active as soon as you call startAnimation() or pauseAnimation(). It will stay in this state until the animations finishes naturally or the stopAnimation() method gets called.
- Stopped: The animator enter this state after calling stopAnimation(). Any animatable properties will stay at their current values. As depicted below, an animator can never return to the active state without being reconfigured and made inactive first.
It is important to know that alterations to the animation can only be made while the animation is inactive.
Now that that is all cleared out, let’s get into the animation. Let’s start by adding a UIPanGestureRecognizer to our view:
The current state of the view will determine whether the animation should run in reverse or not. Let’s use an enum to keep track of this. We also need to create an animator, and store the frame of the view before we started animating. The reason we need this, is that the reversed property can actually only be changed once. Changing it more than once will not have any effect (I do not know if this is intended or not, it’s still a new API). This leaves us with the following interface:
Now let’s translate any changes to the gesture recognizer into positioning the player’s view. We start by handling the start of a pan gesture:
Before we do anything, we make sure the animator isn’t already running. Then, depending on the current state, we initiate a new animator with a specific target frame. This frame will either be the frame of the the view controller’s root view, or the previously saved original frame.
Now we need to handle the continuous changes we get from the gesture recognizer when it is updated (the finger is moved):
We calculate how big the translation was, and then transform that into a progress based on the current state. The next part is important — we do not let the progress hit 0 or 1, it must stay between. Why, you say? Because whenever it reaches 1, it will set as completed and its status will change. The last thing we do is to manually “scrub” the animator by updating its fractionComplete property.
Great, the panning gesture should work now. Dragging up and down will make the player move towards fullscreen and back again. If we stop panning and remove our finger, the view doesn’t animate back to where it is supposed to be. We need to decide the intention of the user, what state was he/she trying to go to?
We can find this by looking at the translation and the velocity of the gesture:
The result of this will be that if you pan more than one third of the view’s height or you ended the gesture with a fast flick (high enough velocity), we continue the animation in whatever way it was going. If not, we reverse the animation back to where you started dragging it. Worth noting is that just as an animator can have many animation blocks, it can also have many completion blocks which will all run on completion. This allows us to dynamically add animations and completion blocks, even after the animator has started running.
The last thing we need to do is to hook these methods to different actions in the gesture recognizer, like so:
That’s all there is to it, folks! We never had to pause the animation, but we could easily have done so if the we wanted an interruptible animation. The project we worked on can be found here:
Please let me know if you have other interesting use cases for UIViewPropertyAnimator, or if you have comments or ideas on my example!