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
There have been many answers to this question and it's been quite awhile now, I have re-chosen the answer to what I believe to be the most relevant now. If there is anyone that thinks otherwise please let me know in comments
How to change the Push and Pop animations in a navigation based app...
If you are new to iOS development. For the simplest, most common animations (such as "slide over" or "one pushes the other") you have to do a huge amount of work.
UIViewControllerAnimatedTransitioning
You need popStyle
boolean - is it popping on, or popping off?
You must include transitionDuration
(trivial) and the main call, animateTransition
You must write the two different animations routines, one for the push, and one for the pop. Inside animateTransition
, simply branch on the boolean popStyle
to one the two routines
The example below does a simple move-over/move-off
In your animatePush
and animatePop
routines. You must get the "from view" and the "to view". (How to do that, is shown in the code example.)
and you must addSubview
for the new "to" view.
and you must call completeTransition
at the end of your anime
Copy-paste ..
class SimpleOver: NSObject, UIViewControllerAnimatedTransitioning {
var popStyle: Bool = false
func transitionDuration(
using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.20
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
if popStyle {
animatePop(using: transitionContext)
return
}
let fz = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!
let tz = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!
let f = transitionContext.finalFrame(for: tz)
let fOff = f.offsetBy(dx: f.width, dy: 55)
tz.view.frame = fOff
transitionContext.containerView.insertSubview(tz.view, aboveSubview: fz.view)
UIView.animate(
withDuration: transitionDuration(using: transitionContext),
animations: {
tz.view.frame = f
}, completion: {_ in
transitionContext.completeTransition(true)
})
}
func animatePop(using transitionContext: UIViewControllerContextTransitioning) {
let fz = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!
let tz = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!
let f = transitionContext.initialFrame(for: fz)
let fOffPop = f.offsetBy(dx: f.width, dy: 55)
transitionContext.containerView.insertSubview(tz.view, belowSubview: fz.view)
UIView.animate(
withDuration: transitionDuration(using: transitionContext),
animations: {
fz.view.frame = fOffPop
}, completion: {_ in
transitionContext.completeTransition(true)
})
}
}
NOTE - at the line of code insertSubview. Notice the comment chain below. If you experience a "black flash" during one of the transitions, that's caused by another problem. It's a common programming difficulty in UIKit that one has a problem loading layers or views (for example, often one is "missing a layout" somewhere in a custom view. However as a "quick fix" as explained by @Dmytro you can also insert the "fz" view, duplicate the insertSubview loc. However take care if you do this as it shouldn't be necessary.
And then ...
Note: strangely, you only have to do this in the "first" view controller. (The one which is "underneath".)
With the one that you pop on top, do nothing. Easy.
So your class...
class SomeScreen: UIViewController {
}
becomes...
class FrontScreen: UIViewController,
UIViewControllerTransitioningDelegate, UINavigationControllerDelegate {
let simpleOver = SimpleOver()
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.delegate = self
}
func navigationController(
_ navigationController: UINavigationController,
animationControllerFor operation: UINavigationController.Operation,
from fromVC: UIViewController,
to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
simpleOver.popStyle = (operation == .pop)
return simpleOver
}
}
That's it.
Push and pop exactly as normal, no change. To push ...
let n = UIStoryboard(name: "nextScreenStoryboardName", bundle: nil)
.instantiateViewController(withIdentifier: "nextScreenStoryboardID")
as! NextScreen
navigationController?.pushViewController(n, animated: true)
and to pop it, you can if you like just do that on the next screen:
class NextScreen: TotallyOrdinaryUIViewController {
@IBAction func userClickedBackOrDismissOrSomethingLikeThat() {
navigationController?.popViewController(animated: true)
}
}
Phew.