So far, you have seen animations that were implemented using the UIView.animate method. These animations are quite simple to implement and mostly follow the following format:
UIView.animate(withDuration: 1.5, animations: { myView.backgroundColor = UIColor.red() })
You have already seen this method implemented in slightly more complex ways, including one that used a closure that was executed upon completion of the animation. For instance, when a user taps on one of the contacts in the Hello-Contacts app, the following code is used to animate a bounce effect:
UIView.animate(withDuration: 0.1, delay: 0, options: [.curveEaseOut], animations: { cell.contactImage.transform = CGAffineTransform(scaleX: 0.9, y: 0.9) }, completion: { finished in UIView.animate(withDuration: 0.1, delay: 0, options: [.curveEaseIn], animations: { cell.contactImage.transform = CGAffineTransform.identity }, completion: { [weak self] finished in self?.performSegue(withIdentifier: "detailViewSegue", sender: self) }) })
It's not particularly pleasing to look at this code. The indentation is all over the place, and a lot is going on in a dense piece of code. If you dissect this code, you will find that the entire animation was implemented in a single method call. While this might be convenient for small animations, it's not very readable. This is especially true if the animation is more complex or if you want to chain several animations, which is the case for the preceding bounce animation.
One reason to favor UIViewPropertyAnimator over the implementation you just saw is readability. Let's see what the same bounce animation looks like when it's refactored to use UIViewPropertyAnimator:
// 1 let downAnimator = UIViewPropertyAnimator(duration: 0.1, curve: .easeOut) { cell.contactImage.transform = CGAffineTransform(scaleX: 0.9, y: 0.9) } let upAnimator = UIViewPropertyAnimator(duration: 0.1, curve: .easeIn) { cell.contactImage.transform = CGAffineTransform.identity } // 2 downAnimator.addCompletion { _ in upAnimator.startAnimation() } upAnimator.addCompletion { [weak self] _ in self?.performSegue(withIdentifier: "detailViewSegue", sender: self) } // 3 downAnimator.startAnimation()
The first thing you should notice is how much longer this code is. The old implementation was only 9 lines, the new one is 17 lines if you count all the blank lines. The second thing you'll notice is how much more readable the code has become. Code readability is something you should never underestimate. You can write great code, but if you come back to it a week later and find yourself struggling with that great piece of code due to bad readability, your code suddenly isn't as great as it was when you first wrote it.
While it's great that UIViewPropertyAnimator makes your code more readable, it still doesn't teach you much about animations. Let's dissect the preceding implementation to see what's happening.
The first section of the preceding code is all about creating instances of UIViewPropertyAnimator. The UIViewPropertyAnimator class has several initializers. The simplest initializer takes no arguments, the duration property can be set later, and by calling the addAnimation method, new animations can be added to the animator. However, that version of the animator would be too basic for Hello-Contacts.
The example code uses a version of UIViewPropertyAnimator that accepts a timing function to make the final bounce animation more lively. If you look at the sample code, the first argument passed to the UIViewPropertyAnimator initializer is the duration of the animation in seconds. The second argument controls the timing function. A timing function describes how an animation should progress over time. For instance, the easeIn option describes how an animation starts off at a slow pace and speeds up over time. The following diagram describes some of the most commonly used timing functions:
In these graphs, the horizontal axis represents the animation's progress. For each graph, the animation timeline is described from left to right on the x-axis. The animation's progress is visualized on the y-axis from bottom to top. At the bottom-left point, the animation hasn't started yet. At the right of the graph, the animation is completely done. The vertical axis represents time.
The final argument that is passed to the UIViewPropertyAnimator initializer is an optional argument for the animation that you wish to execute. This is quite similar to the UIView.animate way of doing things; the most significant difference is that you can add more animations after creating the animator, meaning that nil can be passed as the argument for the animations and you can add animations you wish to execute at a later time. This is quite powerful because you're even allowed to add new animations to UIViewPropertyAnimator while an animation is running!
The second section in the sample code you saw earlier adds completion closures to the animators. The completion closures both receive a single argument. The received argument describes at what point in the animation the completion closure was called. This property will usually have a value of .end, which indicates that the animation ended at the end position. However, this isn't always true because you can finish animations halfway through the animation if you desire. You could also reverse an animation, meaning that the completion position would be .start.
Once the completion closure is added, and the property animators are fully configured, the final step is to start the animation by calling startAnimation() on an animator object. Once the startAnimation() method is called, the animation begins executing immediately. If needed, you can make the animation start with a delay by calling startAnimation(afterDelay:).
After replacing the animation in collectionView(_:didSelectItemAt:) with the UIViewPropertyAnimator version of the tap animation, you should be able to replace the remaining UIView.animate animations in the project. There are two animations in ViewController.swift that are used to animate the changing background color for the collection-view cell when you enter or exit edit mode.
There are also two animations in ContactDetailViewController.swift that you could replace. However, this animation is so short and simple that creating an instance of UIViewPropertyAnimator for it might be a bit much. However, as an exercise, it would be nice to try to find the simplest implementation possible to replace the UIView.animate calls in the ContactDetailViewController class.
Refer to this book's code bundle if you get stuck replacing the animations or want to see the solution to this challenge.