I have a navigation based application and I want to change the animation of the push and pop animations. How would I do that?
Edit 2018
Ther
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
Here is how I have done the same in Swift:
For Push:
UIView.animateWithDuration(0.75, animations: { () -> Void in
UIView.setAnimationCurve(UIViewAnimationCurve.EaseInOut)
self.navigationController!.pushViewController(nextView, animated: false)
UIView.setAnimationTransition(UIViewAnimationTransition.FlipFromRight, forView: self.navigationController!.view!, cache: false)
})
For Pop:
I actually did this a little differently to some of the responses above - but as I am new to Swift development, it might not be right. I have overridden viewWillDisappear:animated:
and added the pop code in there:
UIView.animateWithDuration(0.75, animations: { () -> Void in
UIView.setAnimationCurve(UIViewAnimationCurve.EaseInOut)
UIView.setAnimationTransition(UIViewAnimationTransition.FlipFromLeft, forView: self.navigationController!.view, cache: false)
})
super.viewWillDisappear(animated)
I know this thread is old, but I thought I'd put in my two cents. You don't need to make a custom animation, there's a simple (maybe hacky) way of doing it. Instead of using push, create a new navigation controller, make the new view controller the root view controller of that nav controller, and then present the nav controller from the original nav controller. Present is easily customizable with many styles, and no need to make a custom animation.
For example:
UIViewcontroller viewControllerYouWantToPush = UIViewController()
UINavigationController newNavController = UINavigationController(root: viewControllerYouWantToView)
newNavController.navBarHidden = YES;
self.navigationController.present(newNavController)
And you can change the presentation style however you want.
@Magnus answer, only then for Swift (2.0)
let transition = CATransition()
transition.duration = 0.5
transition.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
transition.type = kCATransitionPush
transition.subtype = kCATransitionFromTop
self.navigationController!.view.layer.addAnimation(transition, forKey: nil)
let writeView : WriteViewController = self.storyboard?.instantiateViewControllerWithIdentifier("WriteView") as! WriteViewController
self.navigationController?.pushViewController(writeView, animated: false)
Some sidenotes:
You can do this as well with Segue, just implement this in prepareForSegue
or shouldPerformSegueWithIdentifier
. However, this will keep the default animation in it as well. To fix this you have to go to the storyboard, click the Segue, and uncheck the box 'Animates'. But this will limit your app for IOS 9.0 and above (atleast when I did it in Xcode 7).
When doing in a segue, the last two lines should be replaced with:
self.navigationController?.popViewControllerAnimated(false)
Even though I set false, it kind of ignores it.
I am not aware of any way you can change the transition animation publicly.
If the "back" button is not necessary you should use modal view controllers to have the "push from bottom" / "flip" / "fade" / (≥3.2)"page curl" transitions.
On the private side, the method -pushViewController:animated:
calls the undocumented method -pushViewController:transition:forceImmediate:
, so e.g. if you want a flip-from-left-to-right transition, you can use
[navCtrler pushViewController:ctrler transition:10 forceImmediate:NO];
You can't change the "pop" transition this way, however.
Just use:
ViewController *viewController = [[ViewController alloc] init];
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:viewController];
navController.navigationBarHidden = YES;
[self presentViewController:navController animated:YES completion: nil];
[viewController release];
[navController release];