Custom UIViewController Transition

I will show with an example:

import UIKit
class PresentingViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .systemBlue
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(presentTapped))
self.view.addGestureRecognizer(tapGestureRecognizer)
}
@IBAction func presentTapped() {
let presented = PresentedViewController()
self.present(presented, animated: true, completion: nil)
}
}
class PresentedViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .systemGreen
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissTapped))
self.view.addGestureRecognizer(tapGestureRecognizer)
}
@objc func dismissTapped() {
self.dismiss(animated: true, completion: nil)
}
}

I have two UIViewControllers, one is to present and the other one is to be presented. They both have UITapGestureRecognizer on their views. When tapped, the presenting one presesents and the presented one dismisses. And the transition is not custom yet.

To make it custom, first we need an animator class which conforms to UIViewControllerAnimatedTransitioning protocol. In this class, we set the duration and the animation of the transition:

import UIKit
class MoveVerticallyAnimator: NSObject, UIViewControllerAnimatedTransitioning {
let duration = 2.0
var presenting = true
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
let fromView = transitionContext.view(forKey: .from)!
let toView = transitionContext.view(forKey: .to)!
let screenHeight = UIScreen.main.bounds.height
let screenWidth = UIScreen.main.bounds.width
toView.frame = CGRect(x: 0, y: presenting ? -screenHeight : screenHeight, width: screenWidth, height: screenHeight)
containerView.addSubview(toView)
UIView.animate(withDuration: duration,
delay: 0.0,
usingSpringWithDamping: 0.6,
initialSpringVelocity: 0.0,
options: []) {
toView.frame = CGRect(x: 0, y: 0, width: screenWidth, height: screenHeight)
fromView.frame = CGRect(x: 0, y: self.presenting ? screenHeight + 20 : -screenHeight – 20, width: screenWidth, height: screenHeight)
} completion: { _ in
transitionContext.completeTransition(true)
}
}
}

Now that we have our transitioning animator ready, we need to make the presenting view controller conform to UIViewControllerTransitioningDelegate and return the animator in its delegate methods. Also while presenting, we need to set transitioningDelegate and modalPresentationStyle:

import UIKit
class PresentingViewController: UIViewController {
let transition = MoveVerticallyAnimator()
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .systemBlue
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(presentTapped))
self.view.addGestureRecognizer(tapGestureRecognizer)
}
@IBAction func presentTapped() {
let presented = PresentedViewController()
presented.transitioningDelegate = self
presented.modalPresentationStyle = .fullScreen
self.present(presented, animated: true, completion: nil)
}
}
extension PresentingViewController: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transition.presenting = true
return transition
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transition.presenting = false
return transition
}
}

And the final transition:

Leave a comment