前两天做了一个过渡动画,为了防止忘记,在这记录一下如何做。

基本概念

UIViewControllerTransitioningDelegate

如图所示,其中绿色的表示from VC,蓝色的表示to VC,通过to VC的方法setTransitioningDelegate:来设置其过渡动画的代理,这个代理需要符合UIViewControllerTransitioningDelegate协议,这个协议提供了做动画需要的animator(这个可以是交互式动画,也可以是非交互式动画)和presentation controller。

Animator:主要做动画的部分都在这里实现,比如动画时间,怎么做等。非交互动画的animator需要遵守UIViewControllerAnimatedTransitioning协议;交互动画同时还要遵守UIViewControllerInteractiveTransitioning。你可以对present和dismiss分别提供相应的animator。

Presentation controller:modalPresentationStyle为UIModalPresentationCustom时,UIKit就会查找自定义的UIPresentationController,此时我们就可以提供一个自定义的来控制其形为;当modalPresentationStyle不为UIModalPresentationCustom时,系统会提供内部已经定义好的UIPresentationController。其作用在下面有说明。

Transitioning Contexts:定义了动画需要的一些元数据,比如动画涉及的VC等。 这些对象需要遵守 UIViewControllerContextTransitioning这个协议,一般来说,这个是由系统创建提供的。以参数形式提供给animator使用。

Transition Coordinators:提供了在做Transition动画的同时,做其它动画的功能,遵循 UIViewControllerTransitionCoordinator 协议。

present的调用顺序

UIKit调用animationControllerForPresentedController:presentingController:sourceController:得到animator,然后如图:

dismiss的调用顺序

UIKit调用 animationControllerForDismissedController: 得到animator,然后如图:

实现自定义Animator

得到动画参数

使用viewControllerForKey: 得到from VC、to VC。

使用containerView得到动画的“容器”。

使用viewForKey:得到要添加或删除的view。

使用finalFrameForViewController:得到VC的最终位置。

做动画

这里提供一个例子:

-(NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
    return self.transitionDuration;
}

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
    // Get the set of relevant objects.
    UIView *containerView = [transitionContext containerView];
    UIViewController *fromVC = [transitionContext
            viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toVC   = [transitionContext
            viewControllerForKey:UITransitionContextToViewControllerKey];

    UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];

    // Set up some variables for the animation.
    CGRect containerFrame = containerView.frame;
    CGRect toViewStartFrame = [transitionContext initialFrameForViewController:toVC];
    CGRect toViewFinalFrame = [transitionContext finalFrameForViewController:toVC];
    CGRect fromViewFinalFrame = [transitionContext finalFrameForViewController:fromVC];

    // Set up the animation parameters.
    if (self.presenting) {
        // Modify the frame of the presented view so that it starts
        // offscreen at the lower-right corner of the container.
        toViewStartFrame.origin.x = containerFrame.size.width;
        toViewStartFrame.origin.y = containerFrame.size.height;
    }
    else {
        // Modify the frame of the dismissed view so it ends in
        // the lower-right corner of the container view.
        fromViewFinalFrame = CGRectMake(containerFrame.size.width,
                                      containerFrame.size.height,
                                      toView.frame.size.width,
                                      toView.frame.size.height);
    }

    // Always add the "to" view to the container.
    // And it doesn't hurt to set its start frame.
    [containerView addSubview:toView];
    toView.frame = toViewStartFrame;

    // Animate using the animator's own duration value.
    [UIView animateWithDuration:[self transitionDuration:transitionContext]
                     animations:^{
                         if (self.presenting) {
                             // Move the presented view into position.
                             [toView setFrame:toViewFinalFrame];
                         }
                         else {
                             // Move the dismissed view offscreen.
                             [fromView setFrame:fromViewFinalFrame];
                         }
                     }
                     completion:^(BOOL finished){
                         BOOL success = ![transitionContext transitionWasCancelled];

                         // After a failed presentation
                         if ((self.presenting && !success)) {
                             [toView removeFromSuperview];
                         }

                         // Notify UIKit that the transition has finished
                         [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
                     }];

}

- (void)animationEnded:(BOOL)transitionCompleted {
    NSLog("transition end");
}

顺带一提,如果要实现一个交互式的animator,最简单的办法是使用一个UIPercentDrivenInteractiveTransition对象。

Presenting a View Controller Using Custom Animations

我们需要以下步骤:

  1. 创建要呈现的VC
  2. 创建自定义transitioning delegate对象,将它赋值给上述VC的 transitioningDelegate property
  3. 调用 presentViewController:animated:completion:呈现上述VC

做transition动画的同时做其它动画

通过相应VC的transitionCoordinator属性得到这个Transition Coordinator,然后调用它的animateAlongsideTransition:completion:animateAlongsideTransitionInView:animation:completion:来在transition的同时做其它动画的目的。

UIPresentationController

使用UIPresentationController可以达到下述目的:

工作顺序

Presentation Controller在动画中的调用顺序如下图,dismiss时只是begin和end调用的函数分别换为dismissalTransitionWillBegin和dismissalTransitionDidEnd:。

定义自己的UIPresentationController

首先,我们应该考虑如下问题:

我们可以通过override frameOfPresentedViewInContainerView来控制presented VC的最终大小。

class SlideInPresentationController: UIPresentationController {
  // MARK: - Properties
  fileprivate var dimmingView: UIView!
  private var direction: PresentationDirection

  init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?, direction: PresentationDirection) {
    self.direction = direction

    super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
    setupDimmingView()
  }

  override func presentationTransitionWillBegin() {
    containerView?.insertSubview(dimmingView, at: 0)

    NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "V:|[dimmingView]|", options: [], metrics: nil, views: ["dimmingView": dimmingView]))
    NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:|[dimmingView]|", options: [], metrics: nil, views: ["dimmingView": dimmingView]))

    guard let coordinator = presentedViewController.transitionCoordinator else {
      dimmingView.alpha = 1.0
      return
    }

    coordinator.animate(alongsideTransition: { _ in
      self.dimmingView.alpha = 1.0
    }, completion: nil)
  }

  override func dismissalTransitionWillBegin() {
    guard let coordinator = presentedViewController.transitionCoordinator else {
      dimmingView.alpha = 0.0
      return
    }

    coordinator.animate(alongsideTransition: { _ in
      self.dimmingView.alpha = 0
    }, completion: nil)
  }

  override func containerViewDidLayoutSubviews() {
    presentedView?.frame = frameOfPresentedViewInContainerView
  }

  override var frameOfPresentedViewInContainerView: CGRect {

    //1
    var frame: CGRect = .zero
    frame.size = size(forChildContentContainer: presentedViewController,
                      withParentContainerSize: containerView!.bounds.size)

    //2
    switch direction {
    case .right:
      frame.origin.x = containerView!.frame.width*(1.0/3.0)
    case .bottom:
      frame.origin.y = containerView!.frame.height*(1.0/3.0)
    default:
      frame.origin = .zero
    }
    return frame
  }

  override func size(forChildContentContainer container: UIContentContainer, withParentContainerSize parentSize: CGSize) -> CGSize {
    switch direction {
    case .left, .right:
      return CGSize(width: parentSize.width*(2.0/3.0), height: parentSize.height)
    default:
      return CGSize(width: parentSize.width, height: parentSize.height*(2.0/3.0))
    }
  }
}


// MARK: - Private
private extension SlideInPresentationController {
  func setupDimmingView() {
    dimmingView = UIView()
    dimmingView.translatesAutoresizingMaskIntoConstraints = false
    dimmingView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
    dimmingView.alpha = 0.0

    let recognizer = UITapGestureRecognizer(target: self, action:#selector(handleTap(recognizer:)))
    dimmingView.addGestureRecognizer(recognizer)
  }

  dynamic func handleTap(recognizer: UITapGestureRecognizer) {
    presentingViewController.dismiss(animated: true)
  }
}
//参考:<https://www.raywenderlich.com/139277/uipresentationcontroller-tutorial-getting-started>

Demo