22.
Getting Started with UIViewPropertyAnimator
Written by Marin Todorov
UIViewPropertyAnimator was introduced in iOS 10 and addressed the need to be able to create easily interactive, interruptible, and/or reversible view animations.
Before iOS 10, the only option to create view-based animations was the UIView.animate(withDuration:...) set of APIs, which did not provide any means for developers to pause or stop already running animations. Further, to reverse, speed up, or slow an animation, developers had to use layer-based CAAnimation animations.
UIViewPropertyAnimator makes creating all of the above a bit easier since it’s a class that lets you keep hold of running animations, lets you adjust the currently running ones and provides you with detailed information about the current state of an animation.
UIViewPropertyAnimator is a big step away from the pre-iOS 10 “fire-and-forget” animations. That being said, UIView.animate(withDuration:...) APIs still do play a big role in creating iOS animations; these APIs are simple and easy to use, and often times you really just want to start a short fade-out or a simple move and you really don’t need to interrupt or reverse those. In these cases using UIView.animate(withDuration:...) is just fine.
Keep in mind that UIViewPropertyAnimator does not implement everything that UIView.animate(withDuration:...) has to offer, so sometimes you will still need to fall back on the old APIs.
Basic animations
Open and run the starter project for this chapter. You should see a screen similar to the lock screen in iOS. The initial view controller displays a search bar, a single widget, and an edit button at the bottom:
Some of the app’s functionality that doesn’t have to do with animations is already implemented for you. For example, if you tap on Show More, you will see the widget expand and show more items. If you tap on Edit you will see another view controller pop up for editing the widget list.
Of course, the app is just a simulation of the lock screen in iOS. It doesn’t actually perform any actions, and is set up specially for you to play with some UIViewPropertyAnimator animations. Let’s go!
First, you are going to create a very simple animation starting when the app initially opens up. Open LockScreenViewController.swift and add a new viewWillAppear(_:) method to that view controller:
override func viewWillAppear(_ animated: Bool) {
tableView.transform = CGAffineTransform(scaleX: 0.67, y: 0.67)
tableView.alpha = 0
}
The table view in LockScreenViewController displays the widgets on that screen, so in order to create a simple scale and fade view animation transition, you first scale the whole table view down and make it transparent.
If you run the project right now you will see the date and some empty space below:
Next, create an animator when the view controller’s view appears on screen. Add the following to LockScreenViewController:
override func viewDidAppear(_ animated: Bool) {
let scale = UIViewPropertyAnimator(duration: 0.33,
curve: .easeIn)
}
Here, you use one of the convenience initializers of UIViewPropertyAnimator. You are going to try all of them, but you’ll start with the simplest one: UIViewPropertyAnimator(duration:, curve:).
This initializer makes an animator instance and sets the animation’s total duration and timing curve. The latter parameter is of type UIViewAnimationCurve, and this is an enum with the following curve-based options:
easeInOuteaseIneaseOutlinear
These match the timing options that you’ve used with the UIView.animate(withDuration:...) APIs, and they produce similar results.
Now that you’ve created an animator object, let’s have a look at what can you do with it.
Adding animations
Add the animation code to viewDidAppear(_:):
scale.addAnimations {
self.tableView.alpha = 1.0
}
You use addAnimations to add blocks of code, which perform the desired animations just like you do with UIView.animate(withDuration:...). The difference when using an animator is that you can add multiple animation blocks. For example, you can include logic to conditionally add more or fewer animations to the same animator.
Besides being able to conditionally build up complex animations, you can also add animations with different delays. There is a version of addAnimations which takes the following two parameters:
-
animation, which is the block with animations to perform, - and
delayFactor, which is the delay before the animations start.
Notice that the latter parameter isn’t called delay, but specifically: delayFactor. This is because you don’t provide an absolute value in seconds, but rather a factor (between 0.0 and 1.0) of the animator’s remaining duration.
Add a second animation to the same animator with some delay:
scale.addAnimations({
self.tableView.transform = .identity
}, delayFactor: 0.33)
To figure out the actual delay in seconds, take delayFactor and multiply it by the remaining duration of the animator. Since you haven’t yet started the animations, the remaining duration is equal to the total duration.
So in the case above:
delayFactor(0.33) * remainingDuration(=duration 0.33) = delay of 0.11 seconds
Why isn’t that second parameter just a simple value in seconds?
Well, imagine your animator is already running, and you decide to add some new animations to it mid-way. In this case the remaining duration will not be equal to the total duration, since some time has already passed since you starter the animations.
In this situation, delayFactor will let you schedule an animation with delay based on the remaining available time. Further, this ensures you cannot set a delay longer than the remaining running time.
Adding completions
Now add a completion block, just like you’re used to with UIView.animate(withDuration:...):
scale.addCompletion { _ in
print("ready")
}
In this simple example, you are only printing to the console, but you can do literally anything you wish; clean up some temporary views, reset the position of visual elements you moved around, and so on.
As with addAnimations(_:), you can call addCompletion(_:) several times to add more completion handlers. These will be executed one after another and in the order you added them to the animator.
Last but not least, you need to start the animator.
Before you call startAnimations(), nothing will happen on screen, so keep in mind while getting accustomed with UIViewPropertyAnimator that if you don’t see your animations on screen, you probably have forgotten to start them.
Add at the end of viewWillAppear(_:):
scale.startAnimation()
Run the project now and enjoy a smooth transition animation when the app pops up on the screen:
Abstracting animations away
You’ve probably already noticed that just like layer animations, animations with UIViewPropertyAnimator add quite a bit of code.
Working with an object that isn’t “fire-and-forget” makes it really easy to extract some of your animation code into a separate class. Since you are going to create plenty of animations for the project in this section of the book, you’ll extract most of them in a separate file.
Create a new file called AnimatorFactory.swift and replace its default contents with:
import UIKit
class AnimatorFactory {
}
Then add a method, which includes the animation code you just wrote, but instead of running the animations by default, returns the animator as the result:
static func scaleUp(view: UIView) -> UIViewPropertyAnimator {
let scale = UIViewPropertyAnimator(duration: 0.33,
curve: .easeIn)
scale.addAnimations {
view.alpha = 1.0
}
scale.addAnimations({
view.transform = CGAffineTransform.identity
}, delayFactor: 0.33)
scale.addCompletion {_ in
print("ready")
}
return scale
}
That method takes a view as its parameter and creates all animations on that view. Finally it returns the ready-to-go animator.
Switch to LockScreenViewController.swift and replace viewDidAppear(_:) with:
override func viewDidAppear(_ animated: Bool) {
AnimatorFactory.scaleUp(view: tableView)
.startAnimation()
}
That’s much nicer, shorter, and cleaner!
By the end of this section, you will really appreciate AnimatorFactory since it’s going to remove a lot of code from your view controller.
Note: In your own projects, you might want to prefer to use an enumeration or a struct to access your abstracted animators. In this book, you’re going to use static class methods.
Running animators
At this point you might be asking yourself “What’s the point of creating an animator object if its only purpose is to be started right away?”
That is a good question!
Should you need a single block of animations that you run and don’t need to alter anymore, go ahead and use UIView.animate(withDuration:...). The tipping point in your decision on which API to use depends whether you want to simply run an animation — or run it and eventually interact with it later on.
What if you do want to use a UIViewPropertyAnimator, but you still have just one block of animations and completion, and want to run it right away? Isn’t there a more streamlined way to create such animations?
Why, yes there is! I’m glad you asked. This is the very reason this section of the chapter is called running animators. There’s a class method on UIViewPropertyAnimator that creates an animator and starts it right away for you.
Next you will fade in a blur layer (blurView) while the user is using the search bar, and fade it out when the user is done searching.
Open LockScreenViewController.swift and add a new method to the LockScreenViewController class:
func toggleBlur(_ blurred: Bool) {
UIViewPropertyAnimator.runningPropertyAnimator(
withDuration: 0.5, delay: 0.1, options: .curveEaseOut,
animations: {
self.blurView.alpha = blurred ? 1 : 0
},
completion: nil
)
}
In toggleBlur(_:) you use UIViewPropertyAnimator.runningPropertyAnimator (withDuration:delay:options:animations:completion ) to create an animator that is already running.
You have certainly noticed that UIViewPropertyAnimator. runningPropertyAnimator(withDuration:...) takes exactly the same parameters as UIView.animate(withDuration:...) to make it easier for you to use this new API.
Even though it looks like this might be a “fire-and-forget” kind of API, please note that it does return an animator instance. You can add more animations, more completion blocks, and generally interact with the animations that are currently running.
Now let’s see what that fade animation looks like. LockScreenViewController is already set as the delegate of the search bar, so you simply need to implement the required methods to trigger the animation at the correct times.
Add a new LockScreenViewController extension:
extension LockScreenViewController: UISearchBarDelegate {
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
toggleBlur(true)
}
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
toggleBlur(false)
}
}
When the users taps on the search field you fade in the blur, and fade it out when the user has finished using the search bar. To give the user more ways to cancel the search, add also these two methods:
func searchBarResultsListButtonClicked(_ searchBar: UISearchBar) {
searchBar.resignFirstResponder()
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
if searchText.isEmpty {
searchBar.resignFirstResponder()
}
}
This will allow the user to dismiss the search by tapping on the right hand side button or by deleting their search query. Run the app now and tap in the search bar text field.
You’ll see the widgets disappear under a blur effect view. When you tap the button on the right side of the search bar, the blur view fades back out.
Basic keyframe animations
Earlier you learned how to add more animation blocks to the same animator and make them start with a given delay factor. This is handy, but it doesn’t do quite the same thing as you’re used to with using view keyframe animations.
The UIView.animateKeyframes API is very powerful as it allows you to group animations in any way, with any kind of delay and duration.
Prepare for some good news!
You can actually use UIView.animate and UIView.animateKeyframes from within your UIViewPropertyAnimator animation blocks.
So in the event you would like to create a complex keyframe animation but still realize the benefits of creating an animator, like being able to pause or reverse, you can!
In this section of the chapter, you are going to create a simple jiggle keyframe animation. You’ll play that animation on any icon the user taps to give them a little visual tap feedback:
Switch to AnimatorFactory.swift and add a new method:
static func jiggle(view: UIView) -> UIViewPropertyAnimator {
return UIViewPropertyAnimator.runningPropertyAnimator(
withDuration: 0.33, delay: 0, animations: {
},
completion: {_ in
}
)
}
Your jiggle animator will run for 0.33 seconds, but still doesn’t really do much. Add the following inside the animations block:
UIView.animateKeyframes(withDuration: 1, delay: 0,
animations: {
UIView.addKeyframe(withRelativeStartTime: 0.0,
relativeDuration: 0.25) {
view.transform = CGAffineTransform(rotationAngle: -.pi/8)
}
UIView.addKeyframe(withRelativeStartTime: 0.25,
relativeDuration: 0.75) {
view.transform = CGAffineTransform(rotationAngle: +.pi/8)
}
UIView.addKeyframe(withRelativeStartTime: 0.75,
relativeDuration: 1.0) {
view.transform = CGAffineTransform.identity
}
},
completion: nil
)
This code defines a view keyframe animation much like the ones you’ve created while working through Chapter 5, “Keyframe Animations”.
The first keyframe rotates the given view to the left, the second rotates it to the right, and finally the third one brings it back home — er, I mean resets its transform.
To make sure the icon remains in its initial position even if the animation was interrupted, add this in the completion block:
view.transform = .identity
There isn’t an obvious way to interrupt the animation, but since you are using an animator, there’s always the possibility to add the code to pause or stop that particular animator later on.
There’s quite a difference between how you think about your animations now, as compared to the UIView.animate(withDuration:...) family of APIs. When using an animator, your animation can always end up completing successfully or being stopped mid-way, or even completing not at its end state, but at its starting state if it was reversed during execution.
Next, since the animation is finalized, you can use the jiggle(view:) method to get the keyframes animator and run it on some views.
Open IconCell.swift (the file is located in the Widget sub-folder). This is the custom collection cell class that displays each of the icons in the widget view:
Whenever one of those cells is selected, you will run the jiggle animator on its image to give the user a bit of touch feedback.
Add a new convenience method on the cell to start an animator on its image:
func iconJiggle() {
AnimatorFactory.jiggle(view: icon)
}
Now Xcode complains that your AnimatorFactory.jiggle method returns a result, but you don’t use it in any way. Luckily that’s an easy problem to fix.
Switch to AnimatorFactory.swift and add the following to the line before static func jiggle(view: UIView) -> UIViewPropertyAnimator a @discardableResult attribute, so that Xcode knows that you might choose to ignore the result of the method:
@discardableResult
static func jiggle(view: UIView) -> UIViewPropertyAnimator
Do not remove the return type altogether — you will use the result of that method later on.
To finally run the animation, open WidgetView.swift and find collectionView(collectionView:didSelectItemAt:). This is the collection view delegate method called when the user taps on a collection view cell.
Append the following to it:
if let cell = collectionView.cellForItem(at: indexPath) as? IconCell {
cell.iconJiggle()
}
Run the app one more time and try tapping on some icons; you will see them shortly jump under your finger:
Note: There currently isn’t a streamlined way to make the animator animation repeat, if you happened to be wondering. If you want a repeating animation you will have to observe the animator’s
runningproperty and once the animation completes, reset the animator’s progress to0%and start it over. Or just use theUIView.animate(withDuration:animations)method.
With this animation, you’ve concluded the basics tour. Hopefully you’ve learned some of the benefits of using UIViewPropertyAnimator over the older APIs.
What you covered, however, is but a small fraction of what UIViewPropertyAnimator can do. In the next chapters, you will look into more interesting ways to set your animations’ timing, interactivity, and power view controller transitions.
Key points
- When using the
UIViewPropertyAnimatortype, you create an object, add animation and/or completion closures to it, and start the animations at your convenience. - Containing animation closures within class instances provides a whole new approach to reusing animations via animation factories.
- To create view keyframe animations, you still need to use the old api
UIView.animateKeyframes(withDuration:delay:keyframes:).
Challenges
You already know some of the basics about working with UIViewPropertyAnimator, but there’s much more to learn in the next three chapters. In this chapter’s challenge section, take the time to reflect on what you’ve learned and experience your first encounter with property animator’s state.
Challenge 1: Extract blur animation into factory
To practice abstracting animations one more time, extract the blur animation from toggleBlur(_:) into a static method on AnimatorFactory.
This time, the static factory method should take two parameters: the view to animate and whether to animate to a fully transparent or fully opaque state.
In the end, you should be able to easily toggle the visibility of blurView by using this one-liner:
func toggleBlur(_ blurred: Bool) {
AnimatorFactory.fade(view: blurView, visible: blurred)
}
Do you appreciate how easy it is to abstract and re-use animations with UIViewPropertyAnimator? I know I certainly do!
Challenge 2: Prevent overlapping animations
In this challenge, you will learn how to check if an animator is currently executing its animations.
If you tap repeatedly on the same icon, you will see that it jumps back to its initial state on each tap and the animations look choppy.
Right now, you simply ignore the result of AnimatorFactory.jiggle — but what if you didn’t? If you actually get hold of the animator object and use it to check if there’s a currently active jiggle animation, you can prevent further taps on that same icon.
First, add an optional property called animator to the IconCell class.
Next, instead of discarding the result of AnimatorFactory.jiggle, store it in animator. Now each time the user taps the icon, you can check if there’s an animation already running on the icon.
At the beginning of iconJiggle() check if animator is set, and if so, check if its isRunning property is true. isRunning tells you if the animator is currently running its animations — i.e., it has been already started but it hasn’t completed yet.
If there’s a running animator, all that is left to do is return out of iconJiggle() without creating a new animation. This will fix your problem and the users can tap as many times on the icon as they wish.
Up next — even more complex animations with UIViewPropertyAnimator!