导航控制器自定义转场动画

74

我一直在跟随一些教程创建自定义动画,实现从一个视图到另一个视图的过渡。

我的测试项目使用这里的自定义segue可以正常工作,但有人告诉我不再鼓励在自定义segue内做自定义动画,应该使用UIViewControllerAnimatedTransitioning

我遵循了几个使用此协议的教程,但它们都是关于模态展示的(例如这个教程)。

我正在尝试在导航控制器树中进行推送(segue),但当我尝试使用show (push) segue时,它就不再起作用了。

请告诉我在导航控制器中从一个视图到另一个视图实现自定义转场动画的正确方法。

还有没有办法可以使用同一个方法实现所有的过渡动画? 如果有一天我想要做同样的动画,最后却不得不将代码复制两次以适用于模态和控制器的过渡,那会很别扭。


@Rob 对不起,如果我说的很蠢,请问如何将我的视图控制器设置为导航控制器的代理?我似乎无法在故事板上绑定它们,而我的“navigationController:animationControllerForOperation:fromViewController:toViewController:”从未被调用。 - AVAVT
你可以在 viewDidLoad 中直接执行 self.navigationController.delegate = self;。除非你还在 @interface 行中指定你的视图控制器符合 <UINavigationControllerDelegate> 协议,否则可能会收到警告。 - Rob
这段代码是用Swift 4编写的,用于实现圆形转场效果。 - Pramod More
4个回答

197

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

  • 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;
      }
      
如果你在谷歌上搜索“UINavigationController自定义转场教程”,你会得到很多结果。或者查看WWDC 2013自定义转场视频

同时如何保留默认的滑动返回手势(屏幕边缘手势)? - kidsid49
@kidsid49 - 当您进行自定义转换时,您会失去内置的交互式弹出手势,但您也可以选择实现自己的交互控制器。请参阅WWDC视频以获取更多信息。请参考有关UIPercentDrivenInteractiveTransition的讨论(它极大地简化了该过程)。我已经添加了一些相关的代码片段。 - Rob
1
@Pavan - 你的animationControllerForOperation可以检查fromVC和/或toVC,如果不想执行自定义动画,则返回nil。最简单的方法是只需检查是否为特定类。更优雅地,你可以设计一个可选协议,使视图控制器符合某些布尔属性以指示它们是否需要自定义推送/弹出。 - Rob
如果有人和我遇到了同样的问题:使用自定义的推送和弹出动画对象将新的视图控制器推入和弹出导航控制器的堆栈会破坏handleSwipeFromLeftEdge的功能。它没有按预期工作(移动得太快,而不是像Finder一样的速度)。如果你有同样的问题,请将UIScreenEdgePanGestureRecognizer添加到其superview属性上,而不是视图本身。这对我解决了问题。不能解释为什么,但它确实有效。 - Baran Emre
在移动一些代码后,它们不再起作用。在您的pop/push Animator对象中,如果要支持百分比驱动交互,请勿使用animate(withDuration:animations:)。请使用transition(with:duration:options:animations:completion:)。这里是一个例子:UIView.transition(with: transitionContext.containerView, duration: animationDuration, options: .curveLinear, animations: { /* 在此更改视图框架/ alpha值 */ }。编写一个使用动画转换方法的动画器对象,并仅在调用handleSwipeFromLeftEdge时返回自定义动画器。 - Baran Emre
显示剩余8条评论

15

addSubview之前,您可能希望添加以下代码

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

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

从苹果文档finalFrameForViewController:

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

此方法返回的矩形表示相应视图在转换结束时的大小。对于在呈现期间被覆盖的视图,此方法返回的值可能为CGRectZero,但也可能是有效的框架矩形。


4
哇,这解决了我的问题。为什么我在其他地方查找(比如苹果文档、其他教程等)都没有看到这个信息呢?对我来说,需要设置这个东西毫无意义。 - David
1
重点是我正在使用自动布局,在其他地方没有设置frame... - David

9

使用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()
    }
}

不要忘记在YourViewController的viewDidLoad()方法中设置委托:
override func viewDidLoad() {
    //...
    self.navigationController?.delegate = self
    //...
}

7

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接