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:
transitioningDelegate
.transitioningDelegate
is asked for an animation controller.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() } }