ios 导航控制器自定义过渡动画

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/26569488/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-31 03:21:51  来源:igfitidea点击:

Navigation controller custom transition animation

ioscocoa-touchcustom-transition

提问by AVAVT

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

我一直在遵循一些教程来创建自定义动画,同时从一个视图过渡到另一个视图。

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

我的测试项目使用这里的自定义 segue工作正常,但有人告诉我不再鼓励在自定义 segue 中执行自定义动画,我应该使用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.

无论如何,我可以对所有过渡动画使用一种方法吗?如果有一天我想做相同的动画,但最终不得不重复两次代码才能处理模态与控制器转换,那将会很尴尬。

回答by Rob

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

要使用导航控制器 ( UINavigationController)进行自定义转换,您应该:

  • Define your view controller to conform to UINavigationControllerDelegateprotocol. For example, you can have a private class extension in your view controller's .mfile 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 animationControllerForOperationin 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 UIPercentDrivenInteractiveTransitionis 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 interactionControllerForAnimationControllerdelegate method

      - (id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
                               interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController {
          return self.interactionController;
      }
      
  • 定义您的视图控制器以符合UINavigationControllerDelegate协议。例如,您可以在视图控制器的.m文件中使用私有类扩展来指定符合此协议:

    @interface ViewController () <UINavigationControllerDelegate>
    
    @end
    
  • 确保您确实将视图控制器指定为导航控制器的委托:

    - (void)viewDidLoad {
        [super viewDidLoad];
    
        self.navigationController.delegate = self;
    }
    
  • 实现animationControllerForOperation您的视图控制器:

    - (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;
    }
    
  • 为推送和弹出动画实现动画,例如:

    @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
    

    这确实淡入淡出过渡,但您应该可以随意自定义动画,因为您认为合适。

  • 如果你想处理交互手势(例如像从左到右的原生滑动弹出),你必须实现一个交互控制器:

    • 为交互控制器(符合 的对象UIViewControllerInteractiveTransitioning)定义一个属性:

      @property (nonatomic, strong) UIPercentDrivenInteractiveTransition *interactionController;
      

      UIPercentDrivenInteractiveTransition是一个很好的对象,它根据手势的完整程度来完成更新自定义动画的繁重工作。

    • 将手势识别器添加到您的视图中。在这里,我只是实现了左手势识别器来模拟弹出:

      UIScreenEdgePanGestureRecognizer *edge = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(handleSwipeFromLeftEdge:)];
      edge.edges = UIRectEdgeLeft;
      [view addGestureRecognizer:edge];
      
    • 实现手势识别处理程序:

      /** 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;
          }
      }
      
    • 在您的导航控制器委托中,您还必须实现interactionControllerForAnimationController委托方法

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

如果你谷歌“UINavigationController 自定义过渡教程”,你会得到很多点击。或查看WWDC 2013 自定义过渡视频

回答by Q i

You may wanna add the following code before addSubview

您可能想在之前添加以下代码 addSubview

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

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

从另一个问题custom-transition-for-push-animation-with-navigationcontroller-on-ios-9

From Apple's Documentation for finalFrameForViewController:

来自 Apple 的 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.

返回指定视图控制器视图的结束帧矩形。

此方法返回的矩形表示过渡结束时对应视图的大小。对于在演示过程中被覆盖的视图,此方法返回的值可能是 CGRectZero 但它也可能是一个有效的框架矩形。

回答by Edi

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

使用 Rob 和 Q i 的完美答案,这里是简化的 Swift 代码,对 .push 和 .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:

不要忘记在 YourViewController 的 viewDidLoad() 方法中设置委托:

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

回答by Sai kumar Reddy

It works both swift 3 and 4

它适用于 swift 3 和 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)
}