Displaying Animation in iOS. Part 2 iOS 19.04.2021

First part is here.

Using UIView.animate and UIViewPropertyAnimator

In their most basic form, animations are simple and easy to use. Here is an example of a typical animation that could be performed:

UIView.animate(withDuration: 0.8) {
    self.sqView.alpha = 1.0
}

Let's take this another step further now and add a bounce effect to sqView when it's tapped. I use SnapKit for view layout.

import UIKit
import SnapKit

class ViewController: UIViewController {

    let sqView: UIView = {
        let v = UIView()
        v.backgroundColor = .red
        v.alpha = 0
        v.isUserInteractionEnabled = true
        return v
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        view.addSubview(sqView)
        sqView.snp.makeConstraints { (make) in
            make.center.equalTo(view)
            make.size.equalTo(200)
        }

        let sqTap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
        sqView.addGestureRecognizer(sqTap)
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        UIView.animate(withDuration: 0.8) {
            self.sqView.alpha = 1.0
        }
    }

    @objc func handleTap() {
        UIView.animate(withDuration: 0.1, delay: 0, options: [.curveEaseOut], animations: {
                self.sqView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
            }, completion: { finished in
                UIView.animate(withDuration: 0.1, delay: 0, options: [.curveEaseOut], animations: {
                    self.sqView.transform = CGAffineTransform.identity
                }, completion: { finished in
                    print("Route to VC")
                })
            })
    }
}

Here, we're extending the .animate function we saw earlier, but this time you see we've got a parameter for a delay and options.

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:

let downAnimator = UIViewPropertyAnimator(duration: 0.1, curve: .easeOut) {
    self.sqView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
}

let upAnimator = UIViewPropertyAnimator(duration: 0.1, curve: .easeIn) {
    self.sqView.transform = CGAffineTransform.identity
}

downAnimator.addCompletion { _ in
    upAnimator.startAnimation()
}

upAnimator.addCompletion { [weak self] _ in
    print("Route to VC")
}
downAnimator.startAnimation()

With the use of UIViewPropertyAnimator, it does exactly as its name describes: it allows you to assign your animation to a property that you can then execute independently within your function.

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.

One of the best features of UIViewPropertyAnimator is that you can use it to create animations that can be interrupted, reversed, or interacted with.

UIKit Dynamics

Most apps implement simple animations, such as the ones you've seen so far. However, some animations might need a little more realism – this is what UIKit Dynamics is for.

With UIKit Dynamics, you can place one or more views in a scene that uses a physics engine to apply certain forces to the views it contains. For instance, you can apply gravity to a particular object, causing it to fall off the screen. You can even have objects bumping into each other, and if you assign a mass to your views, this mass is taken into account when two objects crash into each other.

When you apply a certain force to an object with very little mass, it will be displaced more than an object with a lot of mass, just like you would expect in the real world.

The simplest thing you can implement at this point is to set up a scene that contains the three squares and apply some gravity to them. This will cause the squares to fall off the screen because they'll start falling once gravity is applied, and there is no floor to stop the squares from dropping off the screen.

class ViewController: UIViewController {
    var animator: UIDynamicAnimator?

    let view1: UIView = {
        let v = UIView(frame: CGRect(x: 0, y: 50, width: 100, height: 100))
        v.backgroundColor = .red
        return v
    }()

    let view2: UIView = {
        let v = UIView(frame: CGRect(x: 110, y: 50, width: 100, height: 100))
        v.backgroundColor = .green
        return v
    }()

    let view3: UIView = {
        let v = UIView(frame: CGRect(x: 220, y: 50, width: 100, height: 100))
        v.backgroundColor = .blue
        return v
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        view.addSubview(view1)
        view.addSubview(view2)
        view.addSubview(view3)

        let views: [UIDynamicItem] = [view1, view2, view3]
        animator = UIDynamicAnimator(referenceView: view)
        let gravity = UIGravityBehavior(items: views)
        animator?.addBehavior(gravity)
    }
}

The views in a dynamic scene must be of the UIDynamicItem type. A UIView can be used as UIDynamicItem, so by adding them to a list that has [UIDynamicItem] works automatically.

Then, we create an instance of UIDynamicAnimator and you tell it the view to which it will apply its physics engine. The last step is to configure and apply a behavior. This example uses UIGravityBehavior but there are several other behaviors you can use in your scenes.

Customizing view controller transitions

Implementing a custom modal presentation transition. A lot of applications implement modally presented view controllers. A modally presented view controller is typically a view controller that is presented on top of the current screen as an overlay. By default, modally presented view controllers animate upward from the bottom of the screen and are often used to present forms or other temporary content to the user.

Custom view controller transitions use several objects to facilitate the animation. The first object you will look at is transitioningDelegate for UIViewController. The transitioningDelegate property is responsible for providing an animation controller that provides the custom transition.

The animation controller uses a transitioning context object that provides information about the view controllers that are involved in the transition. Typically, these view controllers will be the current view controller and the view controller that is about to be presented.

A transitioning flow can be described in the following steps:

  1. A transition begins. The target view controller is asked for transitioningDelegate.
  2. transitioningDelegate is asked for an animation controller.
  3. The animation controller is asked for the animation duration.
  4. The animation controller is told to perform the animation.

When the animation is complete, the animation controller calls completeTransition(_:) on the transitioning context to mark the animation as completed. If step 1 or step 2 return nil, or aren't implemented at all, the default animation for the transition is used.

Creating a separate object to control the animation is often a good idea because it allows you to reuse a transition and it keeps your code nice and clean. The animation controller should be an object that conforms to UIViewControllerAnimatedTransitioning. This object will take care of animating the presented view onto the screen.

Let's create the animation controller object. Create a new Cocoa Touch class and name it CustomAnimator (using NSObject as a subclass).

The first method that must be implemented for the animation controller is transitionDuration(using:). The implementation of this method is shown here:

func transitionDuration(using transitionContext:
  UIViewControllerContextTransitioning?) -> TimeInterval {
    return 0.6
}

This method is used to determine the total transition duration in seconds. In this case, the implementation is simple – the animation should last 0.6 seconds.

The second method that needs to be implemented is animateTransition(using:). Its purpose is to take care of the actual animation for the custom transition.

This implementation will take the target view controller and its view will be animated from the top of the screen downward to its final position. It will also do a little bit of scaling, and the opacity of the view will be animated; to do this, UIViewPropertyAnimator will be used.

Now that the animation controller is complete, the UIViewControllerTransitioningDelegate protocol should be implemented on ViewController2 so that it can act as its own transitioningDelegate.

Following is full code.

import UIKit
import SnapKit

class ViewController2: UIViewController, UIViewControllerTransitioningDelegate {
    init() {
        super.init(nibName: nil, bundle: nil)
        transitioningDelegate = self
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemGreen
    }

    func animationController(forPresented presented: UIViewController,
                             presenting: UIViewController,
                             source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return CustomAnimator()
    }

    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return nil
    }
}

class ViewController: UIViewController {
    lazy var btn: UIButton = {
         let btn = UIButton(type: .system)
         btn.setTitle("Go", for: .normal)
         btn.layer.cornerRadius = 8.0
         btn.layer.masksToBounds = true
         btn.tintColor = .white
         btn.backgroundColor = .red
         btn.addTarget(self, action: #selector(handleTap), for: .touchUpInside)
         return btn
     }()

    override func viewDidLoad() {
        super.viewDidLoad()

        view.addSubview(btn)
        btn.snp.makeConstraints { (make) in
            make.center.equalToSuperview()
        }
    }

    @objc func handleTap() {
        let vc = ViewController2()
        vc.modalPresentationStyle = .fullScreen
        self.present(vc, animated: true)
    }
}

class CustomAnimator: NSObject, UIViewControllerAnimatedTransitioning {
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.6
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) else {
            return
        }

        let transitionContainer = transitionContext.containerView

        var transform = CGAffineTransform.identity
        transform = transform.concatenating(CGAffineTransform(scaleX: 0.6, y: 0.6))
        transform = transform.concatenating(CGAffineTransform(translationX: 0, y: -200))
        toViewController.view.transform = transform
        toViewController.view.alpha = 0

        transitionContainer.addSubview(toViewController.view)

        let animationTiming = UISpringTimingParameters(dampingRatio: 0.5,
        initialVelocity: CGVector(dx: 1, dy: 0))
        let animator = UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext),
           timingParameters: animationTiming)
        animator.addAnimations {
            toViewController.view.transform = CGAffineTransform.identity
            toViewController.view.alpha = 1
        }

        animator.addCompletion { finished in
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        }

        animator.startAnimation()
    }
}