{% github it-boyer PresentationsDemo 2cce4c908 width = 30% %}

触发转场的方式

官方支持的自定义转场

  • UINavigationControllerpushpop;
  • UITabBarController 中切换 Tab;
  • Modal 转场:presentationdismissal,俗称视图控制器的模态显示和消失,仅限于modalPresentationStyle属性为 UIModalPresentationFullScreenUIModalPresentationCustom 这两种模式; UICollectionViewController 的布局转场:仅限于 UICollectionViewControllerUINavigationController 结合的转场方式,与上面三种都有点不同,不过实现很简单,可跳转至该链接查看。 官方的支持包含了 iOS 中的大部分转场方式,还有一种自定义容器中的转场并没有得到系统的直接支持,不过借助协议这种灵活的方式,我们依然能够实现对自定义容器控制器转场的定制,在压轴环节我们将实现这一点。

相关触发转场的动作

UINavigationController

UINavigationController 中所有修改其viewControllers栈中 VC 的方法都可以自定义转场动画: {% codeblock swift lang:swift %} //我们使用的最广泛的 push 和 pop 方法 func pushViewController(_ viewController: UIViewController, animated animated: Bool) func popViewControllerAnimated(_ animated: Bool) -> UIViewController? //不怎么常用的 pop 方法 func popToRootViewControllerAnimated(_ animated: Bool) -> [UIViewController]? func popToRootViewControllerAnimated(_ animated: Bool) -> [UIViewController]? //这个方法有有点特别,是对 VC 栈的整体更新,开启动画后的执行比较复杂,具体参考文档说明。不建议在这种情况下开启转场动画。 func setViewControllers(_ viewControllers: [UIViewController], animated animated: Bool) {% endcodeblock %}

UITabBarController

{% codeblock swift lang:swift %} //注意传递的参数必须是其下的子 VC unowned(unsafe) var selectedViewController: UIViewController? var selectedIndex: Int //和上面类似的整体更新 func setViewControllers(_ viewControllers: [UIViewController]?, animated animated: Bool) {% endcodeblock %}

{% codeblock swift lang:swift %} // Presentation 转场 func presentViewController(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion completion: (() -> Void)?) // Dismissal 转场 func dismissViewControllerAnimated(_ flag: Bool, completion completion: (() -> Void)?) {% endcodeblock %}

Segue

storyboard 里设置 segue有两种方式:Button to VC,这种在点击 Button 的时候触发转场;VC to VC,这种需要在代码中调用performSegueWithIdentifier:sender:prepareForSegue:sender:方法是在转场发生前修改转场参数的最后机会。这点对于 Modal 转场比较重要,因为在 storyboard Modal 转场的 Segue 类型不支持选择 Custom 模式,使用 segue 方式触发时必须在prepareForSegue:sender:里修改模式。

iOS 8 的变化

iOS 8 引入了适应性布局,由此添加了两种新的方式来显示一个视图控制器: {% codeblock swift lang:swift %} func showViewController(_ vc: UIViewController, sender sender: AnyObject?) func showDetailViewController(_ vc: UIViewController, sender sender: AnyObject?) {% endcodeblock %} 这两个方法咋看上去是给 UISplitViewController 用的,在 storyboardsegue 的候选模式里,直接给出了Show(e.g. Push)Show Detail(e.g. Replace)这样的提示,以至于我之前一直对这两个 segue 有误解。实际上这两个方法智能判断当前的显示环境来决定如何显示,iOS 8 想统一显示视图控制器的方式,不过引入这两个方法增加了使用的复杂性,来看看这两个方法的使用规则。 这两个方法在 UISplitViewController 上的确是按名字显示的那样去工作的,而在本文关注的控制器上是这样工作的:

ViewControllerNavigationControllerTabBarController
showViewController:sender:PresentationPushPresentation(by self)
showDetailViewController:sender:PresentationPresentation(by self)Presentation(by self)
UINavigationController 重写了showViewController:sender:而执行 push 操作,上面的by self意思是用容器 VC 本身而非其下子 VC 去执行 presentation。这两个方法的行为可以通过重写来改变。
当非容器类 VC 内嵌在这两种容器 VC 里时,会通过最近的容器 VC 来执行:
VC in NavigationControllerVC in TabBarController
:——-:——-:——–
showViewController:sender:Push(by NavigationController)Presentation(by TabBarController)
showDetailViewController:sender:Presentation(by NavigationController)Presentation(by TabBarController)

转场五大工具

iOS 7 以协议的方式开放了自定义转场的 API,协议的好处是不再拘泥于具体的某个类,只要是遵守该协议的对象都能参与转场,非常灵活。转场协议由5种协议组成,在实际中只需要我们提供其中的两个或三个便能实现绝大部分的转场动画:

转场代理(Transition Delegate):

有如下三种容器转场代理,对应上面三种类型的转场: {% codeblock lang:swift %} //UINavigationController 的 delegate 属性遵守该协议。 //UITabBarController 的 delegate 属性遵守该协议。 //UIViewController 的 transitioningDelegate 属性遵守该协议。 {% endcodeblock %} 这里除了是 iOS 7 新增的协议,其他两种在 iOS 2 里就存在了,在 iOS 7 时扩充了这两种协议来支持自定义转场。

动画控制器(Animation Controller):

最重要的部分,负责添加视图以及执行动画;遵守协议;由我们实现。

交互控制器(Interaction Controller):

通过交互手段,通常是手势来驱动动画控制器实现的动画,使得用户能够控制整个过程;遵守协议;系统已经打包好现成的类供我们使用。

转场环境(Transition Context):

提供转场中需要的数据;遵守协议;由 UIKit 在转场开始前生成并提供给我们提交的动画控制器和交互控制器使用。

转场协调器(Transition Coordinator):

可在转场动画发生的同时并行执行其他的动画,其作用与其说协调不如说辅助,主要在 Modal 转场和交互转场取消时使用,其他时候很少用到;遵守协议;由 UIKit 在转场时生成,UIViewController 在 iOS 7 中新增了方法transitionCoordinator()返回一个遵守该协议的对象,且该方法只在该控制器处于转场过程中才返回一个此类对象,不参与转场时返回 nil。

总结下,5个协议只需要我们操心3个;实现一个最低限度可用的转场动画,我们只需要提供上面五个组件里的两个:转场代理和动画控制器即可,还有一个转场环境是必需的,不过这由系统提供;当进一步实现交互转场时,还需要我们提供交互控制器,也有现成的类供我们使用。

特殊的 Modal 转场

容器类 VC 的转场里 fromViewtoViewcontainerView 的子层次的视图,而 Modal 转场里 presentingViewcontainerView 是同层次的视图,只有 presentedViewcontainerView 的子层次视图。

iOS 8引入的UIPresentationController

UIPresentationController类,该类接管了 UIViewController 的显示过程,为其提供转场和视图管理支持。在 iOS 8.0 以上的系统里,你可以在 presentation 转场结束后打印视图控制器的结构,会发现 presentedVC 是由一个UIPresentationController对象来显示的,查看视图结构也能看到 presentedViewUIView 私有子类的UITtansitionView的子视图,这就是前面 containerView 的真面目. 当UIViewControllermodalPresentationStyle属性为.Custom时(不支持.FullScreen),我们有机会通过控制器的转场代理提供UIPresentationController的子类对 Modal 转场进行进一步的定制。实际上该类也可以在.FullScreen模式下使用,但是会丢失由该类负责的动画,保险起见还是遵循官方的建议,只在.Custom模式下使用该类。 UIPresentationController类赋予 Modal 转场以下特性:

  1. 定制 presentedView 的外观,尺寸以及在 containerView 中添加自定义视图并为这些视图添加动画;
  2. 可以选择是否移除 presentingView
  3. 可以在不需要动画控制器的情况下单独工作
  4. iOS 8 中的自适应适应性布局 UIPresentationController类提供了如下的方法参与转场,对转场过程实现了更加细致的控制,从命名便可以看出与动画控制器里的animateTransition:的关系: {% codeblock swift lang:swift %} func presentationTransitionWillBegin() func presentationTransitionDidEnd(_ completed: Bool) func dismissalTransitionWillBegin() func dismissalTransitionDidEnd(_ completed: Bool) {% endcodeblock %} 除了 presentingView,UIPresentationController类拥有转场过程中剩下的角色: {% codeblock swift lang:swift %} //指定初始化方法。 init(presentedViewController presentedViewController: UIViewController, presentingViewController presentingViewController: UIViewController) var presentingViewController: UIViewController { get } var presentedViewController: UIViewController { get } var containerView: UIView? { get } //提供给动画控制器使用的视图,默认返回 presentedVC.view,通过重写该方法返回其他视图,但一定要是 presentedVC.view 的上层视图。 func presentedView() -> UIView?
    {% endcodeblock %} 没有 presentingView 是因为 Custom 模式下 presentingView 不受 containerView 管理,UIPresentationController类并没有改变这一点。iOS 8 扩充了转场环境协议,可以通过viewForKey:方便获取转场的视图,而该方法在 Modal 转场中获取的是presentedView()返回的视图。因此我们可以在子类中将 presentedView 包装在其他视图后重写该方法返回包装后的视图当做 presentedView 在动画控制器中使用。

定制presentedView

外观:重载size方法和frameOfPresentedViewInContainerView属性

重载存储属性:get方法返回登场页面的位置和大小 {% codeblock lang:swift %} override var frameOfPresentedViewInContainerView: CGRect { var presentViewFrame = CGRect.zero let containerBounds = containerView?.bounds //登场控制器内容页面的大小 presentViewFrame.size = size(forChildContentContainer: presentedViewController, withParentContainerSize: (containerBounds?.size)!) presentViewFrame.origin.x = (containerBounds?.size.width)! - presentViewFrame.size.width return presentViewFrame }

//返回登场控制器内容页面的大小,在这里设置为屏幕宽度的三分之一款 override func size(forChildContentContainer container: UIContentContainer, withParentContainerSize parentSize: CGSize) -> CGSize { return CGSize.init(width:CGFloat(floorf(Float(parentSize.width/3.0))), height: parentSize.height) } {% endcodeblock %}

过渡动画,转场协调器(Transition Coordinator)

参与角色都准备好了,但有个问题,无法直接访问动画控制器,不知道转场的持续时间,怎么与转场过程同步?这时候前面提到的用处甚少的转场协调器(Transition Coordinator)将在这里派上用场。该对象可通过 UIViewControllertransitionCoordinator()方法获取,这是 iOS 7 为自定义转场新增的 API,该方法只在控制器处于转场过程中才返回一个与当前转场有关的有效对象,其他时候返回 nil

转场开始
  1. containerView中插入过渡视图chromeView
  2. 为转场中chromeView过渡视图添加转场动画
  3. presentedViewController.transitionCoordinator转场协调器,添加转场的登场和退场动画 {% codeblock presentationTransitionWillBegin lang:swift %} override func presentationTransitionWillBegin() { chromeView.frame = (self.containerView?.bounds)! chromeView.alpha = 0.0 //在containerView中插入视图chromeView
    containerView?.insertSubview(chromeView, at:0) //coordinator转场协调器负责转场动画的呈现和dismissal let coordinator = presentedViewController.transitionCoordinator if (coordinator != nil) { //添加登场动画 coordinator!.animate(alongsideTransition: { (context:UIViewControllerTransitionCoordinatorContext!) -> Void in //animate the alpha to 1.0. self.chromeView.alpha = 1.0 }, completion:nil) } else { chromeView.alpha = 1.0 } } {% endcodeblock %}
转场结束

presentedViewController.transitionCoordinator转场协调器中添加转场的退场动画 {% codeblock dismissalTransitionWillBegin lang:swift %} override func dismissalTransitionWillBegin() { let coordinator = presentedViewController.transitionCoordinator if (coordinator != nil) { //添加退场动画 coordinator!.animate(alongsideTransition: { (context:UIViewControllerTransitionCoordinatorContext!) -> Void in self.chromeView.alpha = 0.0 }, completion:nil) } else { chromeView.alpha = 0.0 } } {% endcodeblock %}

适配屏幕旋转

在设备旋转的情况下,重置背景视图的外观和登场控制器内容的外观 {% codeblock lang:swift %} override func containerViewWillLayoutSubviews() { chromeView.frame = (containerView?.bounds)! presentedView?.frame = frameOfPresentedViewInContainerView } {% endcodeblock %}

Modal的两种PresentationStyle

  1. 设置整个转场动画是否将覆盖全屏幕 .OverFullScreen: 浮动式全屏,即:登场视图下方的视图不会完全被遮挡 .FullScreen : 全覆盖全屏 即:占据全屏来显示登场视图 {% codeblock lang:swift %} //设置整个转场动画是否将覆盖全屏幕 override var shouldPresentInFullscreen: Bool { return true }

override var adaptivePresentationStyle: UIModalPresentationStyle { return UIModalPresentationStyle.fullScreen } {% endcodeblock %}

交互式转场

实现交互化

在非交互转场的基础上将之交互化需要两个条件: 由转场代理提供交互控制器,这是一个遵守协议的对象,不过系统已经打包好了现成的类UIPercentDrivenInteractiveTransition供我们使用。我们不需要做任何配置,仅仅在转场代理的相应方法中提供一个该类实例便能工作。另外交互控制器必须有动画控制器才能工作。 交互控制器还需要交互手段的配合,最常见的是使用手势,或是其他事件,来驱动整个转场进程。

使用一个变量来标记交互状态配合转场交互

如果在转场代理中提供了交互控制器,而转场发生时并没有方法来驱动转场进程(比如手势),转场过程将一直处于开始阶段无法结束。 在两个容器控制器NavigationControllerTabBarController转场为例:

  1. NavigationController 中点击 NavigationBar 也能实现 pop 返回操作,但此时没有了交互手段的支持,转场过程卡壳;
  2. TabBarController 的代理里提供交互控制器存在同样的问题,点击 TabBar 切换页面时也没有实现交互控制。因此仅在确实处于交互状态时才提供交互控制器,可以使用一个变量来标记交互状态,该变量由交互手势来更新状态。

转场动画控制器:向转场中添加视图,执行转场动画

转场 API 是协议的好处是不受限于具体的类,只要对象实现该协议便能参与转场过程,这也带来另外一个好处:封装便于复用,尽管三大转场代理协议的方法不尽相同。 但它们返回的动画控制器遵守的是同一个协议,因此可以将动画控制器封装作为第三方动画控制器在其他控制器的转场过程中使用。 UIViewControllerAnimatedTransitioning代理协议方法,提供了转场所需要的重要数据:

  1. containerView():运行转场动画的容器视图
  2. 转场视图控制器
    • 方法一:viewController(forKey:)UITransitionContextViewControllerKey枚举值:fromto
    • 方法二:viewForKey(_ key: String) -> UIView? AVAILABLE_IOS(8_0):iOS 8新增 API 用于方便获取参与转场的视图.两个键值:UITransitionContextFromViewKey,UITransitionContextToViewKey.

{% codeblock lang:swift %} class ExampleAnimatedTransitioning: NSObject,UIViewControllerAnimatedTransitioning { //used to determine if the presentation animation is presenting (as opposed to dismissing). var isPresentation : Bool = false

//returns the duration in seconds of the transition animation.
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval
{
    //返回动画时间
    return 0.5
}

//get the respective views of these view controllers. 
//Next we get the container view and if the presentation animation is presenting, we add the to view to the container view.
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) 
{
    //get the from and to view controllers from the UIViewControllerContextTransitioning object.
    let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
    let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
    //determine the start and end positions of the view.
    let fromView = fromVC?.view
    let toView = toVC?.view
    let containerView = transitionContext.containerView

    if isPresentation 
    {
        containerView.addSubview(toView!)
    }
    //decide on which view controller to animate based on whether the transition is a presentation or dismissal
    let animatingVC = isPresentation ? toVC : fromVC
    let animatingView = animatingVC?.view

    let finalFrameForVC = transitionContext.finalFrame(for: animatingVC!)
    var initialFrameForVC = finalFrameForVC
    //This will animate the view from right to left during a presentation and vice versa during dismissal.
    initialFrameForVC.origin.x += initialFrameForVC.size.width

    let initialFrame = isPresentation ? initialFrameForVC : finalFrameForVC
    let finalFrame = isPresentation ? finalFrameForVC : initialFrameForVC

    animatingView?.frame = initialFrame
    //根据协议中的方法获取动画的时间。
    let duration = transitionDuration(using: transitionContext)
    UIView.animate(withDuration: duration, delay:0, usingSpringWithDamping:300.0, initialSpringVelocity:5.0, options:UIViewAnimationOptions.allowUserInteraction, animations:{

        //we move the view to the final position.
        animatingView?.frame = finalFrame

    }, completion:{ (value: Bool) in

        if !self.isPresentation {
            //If the transition is a dismissal, we remove the view.
            fromView?.removeFromSuperview()
        }
        //we complete the transition by calling transitionContext.completeTransition()
        transitionContext.completeTransition(true)
    })
}
//    UIView.transitionFromView(fromView, toView: toView, duration: durantion, options: .TransitionCurlDown, completion: { _ in
//    let isCancelled = transitionContext.transitionWasCancelled()
//    transitionContext.completeTransition(!isCancelled)
//    })

//如果实现了,会在转场动画结束后调用,可以执行一些收尾工作。
func animationEnded(_ transitionCompleted: Bool) {
    //
}

} {% endcodeblock %}

转场代理协议(Transition Delegate)

自定义转场的第一步便是提供转场代理,告诉系统使用我们提供的代理而不是系统的默认代理来执行转场。

实现转场代理协议方法,整合动画控制器和自定义展示控制器

  1. 返回管理用户信息视图控制器如何展示的控制器。前面实现的ExamplePresentationViewController类可同时处理 presentation转场 和 dismissal 转场。
  2. 动画控制器为 presentationdismissal 转场分别提供了动画控制器。

UIPresentationController只在 iOS 8中可用,通过available关键字可以解决 API 的版本差异。

{% codeblock lang:swift %} class ExampleTransitioningDelegate: NSObject,UIViewControllerTransitioningDelegate { //returns a presentation controller that manages the presentation of a view controller. func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { //presentation动画控制器 let presentationController = ExamplePresentationViewController(presentedViewController:presented, presenting:presenting)

    return presentationController
}

//为presentation转场提供登场转场动画控制器
func animationController(forPresented presented: UIViewController, 
                                     presenting: UIViewController, 
                                         source: UIViewController) -> UIViewControllerAnimatedTransitioning? 
{
    //登场转场动画控制器
    let animator = ExampleAnimatedTransitioning()
    animator.isPresentation = true
    return animator
}

//为dismissal 转场提供退场转场动画控制器
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? 
{
    //退场转场动画控制器
    let animator = ExampleAnimatedTransitioning()
    animator.isPresentation = false
    return animator
}

} {% endcodeblock %}

使用自定义的转场的代理

自定义转场的第一步便是提供转场代理,告诉系统使用我们提供的代理而不是系统的默认代理来执行转场。 UIViewControllerTransitioningDelegate转场代理:

  1. 强引用代理变量:强引用的变量来维护该代理
  2. Modal转场代理的特性:由presentedVC自身来遵循转场代理presentedVC.modalPresentationStyle,和前两个容器控制器转场代理不同。
  3. 两种支持自定义转场模式:.Custom.FullScreen,默认值为.FullScreen {% codeblock fromVC.class lang:swift %} //强引用的变量来维护该代理 let exampleTransitionDelegate = ExampleTransitioningDelegate() //create an instance of ExampleViewController which will provide the content to display. let presentedVC = ExampleViewController() presentedVC.modalPresentationStyle = .custom presentedVC.transitioningDelegate = exampleTransitionDelegate

//present this view controller. present(toVC, animated: true, completion: nil) {% endcodeblock %}

两种常规的转场方式

UIView方式:transitionFromView

不需要获取 containerView 以及手动添加 toView 就能实现一个指定类型的转场动画,而缺点则是只能使用指定类型的动画。 {% codeblock lang:swift %} UIView.transitionFromView(fromView, toView: toView, duration: durantion, options: .TransitionCurlDown, completion: { _ in let isCancelled = transitionContext.transitionWasCancelled() transitionContext.completeTransition(!isCancelled) }) {% endcodeblock %}

UIViewController方式:在子 VC 间转换的方法

该方法用 toVC 的视图转换 fromVC 的视图在父视图中的位置,并且执行animations闭包里的动画。 {% codeblock lang:swift %} transitionFromViewController:toViewController:duration:options:animations:completion: {% endcodeblock %} 该方法仅限于在自定义容器控制器里使用,如果直接使用 UINavigationControllerUITabBarController 调用该方法执行子VC间转换会抛出异常。

不过 iOS 7 中这两个容器控制器开放的自定义转场做的是同样的事情,回头再看第一章 Transition 解释,转场协议 API 将这个方法拆分成了上面的几个组件,并且加入了激动人心的交互控制,以便我们能够方便定制转场动画。

原文