Search code examples
iosanimationuinavigationcontroller

How to change the Push and Pop animations in a navigation based app


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


Solution

  • Modern 2022 code.

    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.

    1. You need a custom UIViewControllerAnimatedTransitioning

    1. You need popStyle boolean - is it popping on, or popping off?

    2. You must include transitionDuration (trivial) and the main call, animateTransition

    3. 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

    4. The example below does a simple move-over/move-off

    5. 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.)

    6. and you must addSubview for the new "to" view.

    7. 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 ...

    2. Use it in your view controller.

    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.