Since this is the top result on Google I thought I'd share what I think is the most sane way; which is to use the iOS 7+ transitioning API. I implemented this for iOS 10 with Swift 3.
It's pretty simple to combine this with how UINavigationController
animates between two view controllers if you create a subclass of UINavigationController
and return an instance of a class that conforms to the UIViewControllerAnimatedTransitioning
protocol.
For example here is my UINavigationController
subclass:
class NavigationController: UINavigationController {
init() {
super.init(nibName: nil, bundle: nil)
delegate = self
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension NavigationController: UINavigationControllerDelegate {
public func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return NavigationControllerAnimation(operation: operation)
}
}
You can see that I set the UINavigationControllerDelegate
to itself, and in an extension on my subclass I implement the method in UINavigationControllerDelegate
that allows you to return a custom animation controller (i.e., NavigationControllerAnimation
). This custom animation controller will replace the stock animation for you.
You're probably wondering why I pass in the operation to the NavigationControllerAnimation
instance via its initializer. I do this so that in NavigationControllerAnimation
's implementation of the UIViewControllerAnimatedTransitioning
protocol I know what the operation is (i.e., 'push' or 'pop'). This helps to know what kind of animation I should do. Most of the time, you want to perform a different animation depending on the operation.
The rest is pretty standard. Implement the two required functions in the UIViewControllerAnimatedTransitioning
protocol and animate however you like:
class NavigationControllerAnimation: NSObject, UIViewControllerAnimatedTransitioning {
let operation: UINavigationControllerOperation
init(operation: UINavigationControllerOperation) {
self.operation = operation
super.init()
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.3
}
public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) else { return }
let containerView = transitionContext.containerView
if operation == .push {
// do your animation for push
} else if operation == .pop {
// do your animation for pop
}
}
}
It's important to remember, that for each different type of operation (i.e., 'push' or 'pop), the to and from view controllers will be different. When you are in a push operation, the to view controller will be the one being pushed. When you are in a pop operation, the to view controller will be the one that is being transitioned to, and the from view controller will be the one that's being popped.
Also, the to
view controller must be added as a subview of the containerView
in the transition context.
When your animation completes, you must call transitionContext.completeTransition(true)
. If you are doing an interactive transition, you will have to dynamically return a Bool
to completeTransition(didComplete: Bool)
, depending on if the transition is complete at the end of the animation.
Finally (optional reading), you might want to see how I did the transition I was working on. This code is a bit more hacky and I wrote it pretty quickly so I wouldn't say it's great animation code but it still shows how to do the animation part.
Mine was a really simple transition; I wanted to mimic the same animation that UINavigationController typically does, but instead of the 'next page over the top' animation it does, I wanted to implement a 1:1 animation of the old view controller away at the same time as the new view controller appears. This has the effect of making the two view controllers seem as though they are pinned to each other.
For the push operation, that requires first setting the toViewController
's view origin on the x–axis off screen, adding it as the subview of the containerView
, animating it onto screen by setting that origin.x
to zero. At the same time, I animate the fromViewController
's view away by setting its origin.x
off the screen:
toViewController.view.frame = containerView.bounds.offsetBy(dx: containerView.frame.size.width, dy: 0.0)
containerView.addSubview(toViewController.view)
UIView.animate(withDuration: transitionDuration(using: transitionContext),
delay: 0,
options: [ UIViewAnimationOptions.curveEaseOut ],
animations: {
toViewController.view.frame = containerView.bounds
fromViewController.view.frame = containerView.bounds.offsetBy(dx: -containerView.frame.size.width, dy: 0)
},
completion: { (finished) in
transitionContext.completeTransition(true)
})
The pop operation is basically the inverse. Add the toViewController
as a subview of the containerView
, and animate away the fromViewController
to the right as you animate in the toViewController
from the left:
containerView.addSubview(toViewController.view)
UIView.animate(withDuration: transitionDuration(using: transitionContext),
delay: 0,
options: [ UIViewAnimationOptions.curveEaseOut ],
animations: {
fromViewController.view.frame = containerView.bounds.offsetBy(dx: containerView.frame.width, dy: 0)
toViewController.view.frame = containerView.bounds
},
completion: { (finished) in
transitionContext.completeTransition(true)
})
Here's a gist with the whole swift file:
https://gist.github.com/alanzeino/603293f9da5cd0b7f6b60dc20bc766be