Navigation controller custom transition animation

IosCocoa TouchCustom Transition

Ios Problem Overview


I've been following some tutorials to create custom animation while transitioning from one view to another.

My test project using custom segue from here works fine, but someone told me it's not encouraged anymore to do custom animation within a custom segue, and I should use UIViewControllerAnimatedTransitioning.

I followed several tutorials that make use of this protocol, but all of them are about modal presentation (for example this tutorial).

What I'm trying to do is a push segue inside a navigation controller tree, but when I try to do the same thing with a show (push) segue it doesn't work anymore.

Please tell me the correct way to do custom transitioning animation from one view to another in a navigation controller.

And is there anyway I can use one method for all transitioning animations? It would be awkward if one day I want to do the same animation but end up having to duplicate the code twice to work on modal vs controller transitioning.

Ios Solutions


Solution 1 - Ios

To do a custom transition with navigation controller (UINavigationController), you should:

  • Define your view controller to conform to UINavigationControllerDelegate protocol. For example, you can have a private class extension in your view controller's .m file that specifies conformance to this protocol:

      @interface ViewController () <UINavigationControllerDelegate>
    
      @end
    
  • Make sure you actually specify your view controller as your navigation controller's delegate:

      - (void)viewDidLoad {
          [super viewDidLoad];
          
          self.navigationController.delegate = self;
      }
    
  • Implement animationControllerForOperation in your view controller:

      - (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
                                        animationControllerForOperation:(UINavigationControllerOperation)operation
                                                     fromViewController:(UIViewController*)fromVC
                                                       toViewController:(UIViewController*)toVC
      {
          if (operation == UINavigationControllerOperationPush)
              return [[PushAnimator alloc] init];
          
          if (operation == UINavigationControllerOperationPop)
              return [[PopAnimator alloc] init];
          
          return nil;
      }
    
  • Implement animators for push and pop animations, e.g.:

      @interface PushAnimator : NSObject <UIViewControllerAnimatedTransitioning>
    
      @end
    
      @interface PopAnimator : NSObject <UIViewControllerAnimatedTransitioning>
    
      @end
    
      @implementation PushAnimator
    
      - (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext
      {
          return 0.5;
      }
    
      - (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
      {
          UIViewController* toViewController   = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
          
          [[transitionContext containerView] addSubview:toViewController.view];
          
          toViewController.view.alpha = 0.0;
          
          [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
              toViewController.view.alpha = 1.0;
          } completion:^(BOOL finished) {
              [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
          }];
      }
    
      @end
    
      @implementation PopAnimator
    
      - (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext
      {
          return 0.5;
      }
    
      - (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
      {
          UIViewController* toViewController   = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
          UIViewController* fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
          
          [[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view];
          
          [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
              fromViewController.view.alpha = 0.0;
          } completion:^(BOOL finished) {
              [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
          }];
      }
    
      @end
    

That does fade transition, but you should feel free to customize the animation as you see fit.

  • If you want to handle interactive gestures (e.g. something like the native swipe left-to-right to pop), you have to implement an interaction controller:

  • Define a property for an interaction controller (an object that conforms to UIViewControllerInteractiveTransitioning):

         @property (nonatomic, strong) UIPercentDrivenInteractiveTransition *interactionController;
    

    This UIPercentDrivenInteractiveTransition is a nice object that does the heavy lifting of updating your custom animation based upon how complete the gesture is.

  • Add a gesture recognizer to your view. Here I'm just implementing the left gesture recognizer to simulate a pop:

         UIScreenEdgePanGestureRecognizer *edge = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(handleSwipeFromLeftEdge:)];
         edge.edges = UIRectEdgeLeft;
         [view addGestureRecognizer:edge];
    
  • Implement the gesture recognizer handler:

         /** Handle swipe from left edge
          *
          * This is the "action" selector that is called when a left screen edge gesture recognizer starts.
          *
          * This will instantiate a UIPercentDrivenInteractiveTransition when the gesture starts,
          * update it as the gesture is "changed", and will finish and release it when the gesture
          * ends.
          *
          * @param   gesture       The screen edge pan gesture recognizer.
          */
    
         - (void)handleSwipeFromLeftEdge:(UIScreenEdgePanGestureRecognizer *)gesture {
             CGPoint translate = [gesture translationInView:gesture.view];
             CGFloat percent   = translate.x / gesture.view.bounds.size.width;
             
             if (gesture.state == UIGestureRecognizerStateBegan) {
                 self.interactionController = [[UIPercentDrivenInteractiveTransition alloc] init];
                 [self popViewControllerAnimated:TRUE];
             } else if (gesture.state == UIGestureRecognizerStateChanged) {
                 [self.interactionController updateInteractiveTransition:percent];
             } else if (gesture.state == UIGestureRecognizerStateEnded) {
                 CGPoint velocity = [gesture velocityInView:gesture.view];
                 if (percent > 0.5 || velocity.x > 0) {
                     [self.interactionController finishInteractiveTransition];
                 } else {
                     [self.interactionController cancelInteractiveTransition];
                 }
                 self.interactionController = nil;
             }
         }
    
  • In your navigation controller delegate, you also have to implement interactionControllerForAnimationController delegate method

         - (id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
                                  interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController {
             return self.interactionController;
         }
    

If you google "UINavigationController custom transition tutorial" and you'll get many hits. Or see WWDC 2013 Custom Transitions video.

Solution 2 - Ios

You may wanna add the following code before addSubview

  toViewController.view.frame = [transitionContext finalFrameForViewController:toViewController];

From another question custom-transition-for-push-animation-with-navigationcontroller-on-ios-9

From Apple's Documentation for finalFrameForViewController:

> Returns the ending frame rectangle for the specified view controller’s > view. > > The rectangle returned by this method represents the size of the > corresponding view at the end of the transition. For the view being > covered during the presentation, the value returned by this method > might be CGRectZero but it might also be a valid frame rectangle.

Solution 3 - Ios

Using Rob's & Q i's perfect answers, here is the simplified Swift code, using the same fade animation for .push and .pop:

extension YourViewController: UINavigationControllerDelegate {
    func navigationController(_ navigationController: UINavigationController,
                              animationControllerFor operation: UINavigationControllerOperation,
                              from fromVC: UIViewController,
                              to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
							  
	    //INFO: use UINavigationControllerOperation.push or UINavigationControllerOperation.pop to detect the 'direction' of the navigation
	    
        class FadeAnimation: NSObject, UIViewControllerAnimatedTransitioning {
	        func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
	            return 0.5
	        }
        
	        func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
	            let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
	            if let vc = toViewController {
                    transitionContext.finalFrame(for: vc)
                    transitionContext.containerView.addSubview(vc.view)
                    vc.view.alpha = 0.0
                    UIView.animate(withDuration: self.transitionDuration(using: transitionContext),
                    animations: {
                        vc.view.alpha = 1.0
                    },
                    completion: { finished in
                        transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
                    })
                } else {
                    NSLog("Oops! Something went wrong! 'ToView' controller is nill")
                }
	        }
	    }
		
	    return FadeAnimation()
	}
}

Do not forget to set the delegate in YourViewController's viewDidLoad() method:

override func viewDidLoad() {
    //...
    self.navigationController?.delegate = self
    //...
}

Solution 4 - Ios

It works both swift 3 and 4

@IBAction func NextView(_ sender: UIButton) {
  let newVC = self.storyboard?.instantiateViewControllerWithIdentifier(withIdentifier: "NewVC") as! NewViewController
        
  let transition = CATransition()
  transition.duration = 0.5
  transition.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
  transition.type = kCATransitionPush
  transition.subtype = kCAGravityLeft
  //instead "kCAGravityLeft" try with different transition subtypes
                
  self.navigationController?.view.layer.add(transition, forKey: kCATransition)
  self.navigationController?.pushViewController(newVC, animated: false)
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAVAVTView Question on Stackoverflow
Solution 1 - IosRobView Answer on Stackoverflow
Solution 2 - IosQ iView Answer on Stackoverflow
Solution 3 - Iosuser6338195View Answer on Stackoverflow
Solution 4 - IosSai kumar ReddyView Answer on Stackoverflow