The process of extracting the bounce animation is a little more complicated than the process of extracting the contact-fetching code. The purpose of extracting this bounce animation is to make it so that it becomes possible to make other objects in other sections of the app bounce, just like the contact cell's image does.
To figure out what the bounce animation helper should do and how it should work, it's a great idea to think about how you want to use this helper at the call site. The call site is defined as the place where you plan to use your helper. So, in this case, the call site is considered the ViewController. Let's write some pseudocode to try to determine what you will program later:
let onBounceComplete = { [weak self] finished in self?.performSegue(withIdentifier: "contactDetailSegue", sender: self) } let bounce = BounceAnimationHelper(targetView: cell.contactImage, onComplete: onBounceComplete)
This looks pretty good already, and in reality, it's very close to the actual code you could end up with later. All you really want to do at the call site is configure a bounce animation by passing it a view to perform the bounce on and to have some control over what should happen once the animation is completed. The following two points should be considered before writing the BounceAnimationHelper implementation:
- It's not possible to set the bounce duration – is this acceptable?
- You have no control over the start time of the animation, so manually starting it would be nice.
To address the first point, you could implement two initializers: one that uses a default duration and another where the users of the helper can specify their own duration. Doing this makes use of Swift's powerful method overloading, which enables developers to write multiple initializers for the same object. This also allows developers to write methods with the same name but a different signature due to different parameter names.
The second point of concern is valid, and the helper should be written in a way that requires manually starting the animation, so it feels more like UIViewPropertyAnimator. Theoretically speaking, you could add a Bool value to the initializer that enables users of the helper to choose whether the animation should start automatically. This would make it less similar to UIViewPropertyAnimator so in this case manually calling a method to start the animation is preferred. The calling code you should end up with is shown in the following code snippet. You can go ahead and add it to the ViewController.swift file in place of the current bounce animation. You'll implement the helper shortly:
let onBounceComplete: BounceAnimationHelper.BounceAnimationComplete = { [unowned self] position in self.performSegue(withIdentifier: "detailViewSegue", sender: self) } let bounce = BounceAnimationHelper(targetView: cell.contactImage, onComplete: onBounceComplete) bounce.startAnimation()
Note that the preceding snippet explicitly defines the type of the onBounceComplete callback. This type will be a typealias on the helper you're about to implement.
Now that the call site is all finished, let's take a look at the implementation of BounceAnimationHelper. Create a new Swift file called BounceAnimationHelper and add it to the Helpers folder. Start by defining a struct named BounceAnimationHelper in the corresponding Swift file. Next, define a typealias for the completion handler and specify the properties we need in the struct, as follows:
import UIKit struct BounceAnimationHelper { typealias BounceAnimationComplete = (UIViewAnimatingPosition) -> Void let animator: UIViewPropertyAnimator }
The initial implementation for the struct is pretty bare. A typealias is defined that passes a UIViewAnimatingPosition into a closure that has no return value. The struct also holds onto a UIViewPropertyAnimator, so its startAnimation() can be called whenever the helper's startAnimation() is called.
Next, let's add the two initializers that were described earlier. One with a default duration for the bounce effect and one with a custom duration. The second initializer is empty for now; you will implement it in a minute:
init(targetView: UIView, onComplete: @escaping BounceAnimationComplete) { self.init(targetView: targetView, onComplete: onComplete, duration: 0.4) } init(targetView: UIView, onComplete: @escaping BounceAnimationComplete, duration: TimeInterval) { }
These two initializers provide the APIs you were looking for. The first initializer calls out to the second with a default duration value of 0.4. Doing this allows you to write the animation in a single initializer. It is common to have multiple initializers on an object that all trickle down to a single initializer; the designated initializer. The designated initializer is responsible for setting up and configuring all properties of an object. The following code shows the implementation for the designated initializer. It replaces the empty initializer you saw in the previous snippet:
init(targetView: UIView, onComplete: @escaping BounceAnimationComplete, duration: TimeInterval) { let downAnimationTiming = UISpringTimingParameters(dampingRatio: 0.9, initialVelocity: CGVector(dx: 20, dy: 0)) // 1 self.animator = UIViewPropertyAnimator(duration: duration/2, timingParameters: downAnimationTiming) self.animator.addAnimations { // 2 targetView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9) } self.animator.addCompletion { position in let upAnimationTiming = UISpringTimingParameters(dampingRatio: 0.3, initialVelocity:CGVector(dx: 20, dy: 0)) let upAnimator = UIViewPropertyAnimator(duration: duration/2, timingParameters: upAnimationTiming) upAnimator.addAnimations { targetView.transform = CGAffineTransform.identity } // 2 upAnimator.addCompletion(onComplete) upAnimator.startAnimation() } }
This snippet is very similar to the old animation; the main differences are highlighted with comments. Instead of hardcoding a duration, half of the total duration of the bounce animation is used for the downward motion, and the other half is for the upward motion. Also, instead of using the cell's image directly, the specified targetView is used as the animation target.
Finally, instead of passing an inline callback to the upAnimator closure's completion, the onComplete closure that was passed to the initializer is passed to upAnimator. Note that the down animation isn't started in the initializer; this should be implemented in a separate method:
func startAnimation() { animator.startAnimation() }
Add the preceding method to BounceAnimationHelper and run your app. The contact images should bounce just like they did before, except the animation is reusable now and the code in ViewController.swift looks a lot cleaner.
With the cleaned-up ViewController in place, let's see where Hello-Contacts could benefit from protocols.