Custom UINavigationController animated transitions

Thanks to the iOS Animation Tutorial: Custom View Controller Presentation Transitions article combined with A look at UIView Animation Curves (Part 3), it’s not that bad to implement custom transitions on UINavigationControllers. But of course, it’s sprinkling the right ingredients in the right proportions in the right files…

Create an Animator

Let’s start with the animation. If you create a file like SlideAnimator.swift and you put in it:

import UIKit

class SlideAnimator: NSObject, UIViewControllerAnimatedTransitioning {
    var duration: TimeInterval = 1.0

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

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        let containerView = transitionContext.containerView
        let toView = transitionContext.view(forKey: .to)!

        containerView.addSubview(toView)
        toView.frame = CGRect(x: 0, y: containerView.bounds.height, width: containerView.bounds.width, height: containerView.bounds.height)

        let timingFunction = CAMediaTimingFunction(controlPoints: 0/6, 0.8, 3/6, 1.0)
        CATransaction.begin()
        CATransaction.setAnimationTimingFunction(timingFunction)
        UIView.animate(withDuration: duration, animations: {
            toView.frame = containerView.frame
        }, completion: { _ in
            transitionContext.completeTransition(true)
        } )
        CATransaction.commit()
    }
}

That will take the “to view” that you’re going to and slide it up from the offscreen bottom and overlay the “from view”. In the above code, we do it in 1 second—tune to your liking. In animateTransition there’s a lot of juicy stuff in there:

  • The “to view” needs to be added to the containerView.
  • The transitionContext view needs to be added to the controller’s view.
  • We set the toView offscreen beyond the bottom.
  • We use a Bezier curve timing curve and add that to the CATransaction.
  • We do a usual UIView.animate and position the toView where we eventually want it.
  • The completion block notifies the transition context that it’s done.
  • And then we commit() to begin the animation.

Tell the UINavigationController to use custom transitions

(This does assume you are instantiating UINavigationController programmatically, and that it’s not your root view controller.) In the view controller that hosts your UINavigationController, set its delegate to itself, e.g.

class ViewController: UIViewController {
    var navVC: UINavigationController!
    ...

    override func viewDidLoad() {
        ...
        navVC = UINavigationController(rootViewController: tableVC)
        navVC.delegate = self
        ...
    }
}

Then it’s just a matter of extending that view controller to asking it for an animation controller depending on the operation (push or pop):

extension ViewController: UINavigationControllerDelegate {
    func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        switch operation {
        case .push:
            return SlideAnimator()
        default:
            return nil
        }
    }
}

Believe it or not, that is it!

Pssst. If you want to see how to go from UIViewController to UIViewController without a UINavigationController, you can do custom animations with present().