创制自定义presentation,视图控制器转场详解

ca88会员登录中心 10
ca88会员登录中心

ca88会员登录中心 1图片来自网络

前言的前言

UIKit将视图控制器的内容与内容被呈现和显示在屏幕上的方式分开。呈现的视图控制器由底层的presentation
controller对象管理,该对象管理用于显示视图控制器的视图的视觉样式。presentation
controller可以执行以下操作:

iOS相比,在macOS中,控制器的转场情景相对要简洁一些,没有iOS中导航控制器的PushPop动画以及边缘返回手势,
保留下的Present方式,倒是提供了特有的切换方式,
可以供我们使用出许多效果.

关于NSViewController基础细节,有兴趣的同学可以参考我的Mac开发基础教程这个系列的教程,友情提示: 自学能力好的同学可以参考github中的课程代码.另外一门macOS
应用开发进阶课程,供有项目经验或对组件化感兴趣的同学参考.

唐巧前辈在微信公众号「iOSDevTips」以及其博客上推送了我的文章后,我的
Github
各项指标有了大幅度的增长,多谢唐巧前辈的推荐。有些人问我相关的问题,好吧,目前为止就几个,由于没有评论系统,实在不方便交流,但我也没把博客好好整理,一直都在简书上写博客,大家有问题请移步我的简书本文章的页面。关于交流,我想说这么几点:

  • 设置所呈现的视图控制器的尺寸。
  • 添加自定义视图来更改所呈现内容的视觉外观。
  • 为其任何自定义视图提供转场动画。
  • 当应用程序的屏幕环境发生变化时,调整所呈现内容的视觉外观。

macOS 10.10之后,关于NSViewController,苹果公司专门在一个extension中提供了四个方法用来处理控制器之间的关系以及切换转场处理.

1.问问题就好,不要加上大神大牛之类的称呼,与本文有关的问题我尽量回答;不负责解析转场动画,看心情回答。

UIKit为标准呈现样式提供了presentation
controller,当我们将视图控制器的呈现样式设置为UIModalPresentationCustom并提供合适的转场动画委托时,UIKit会改为使用我们自定义的presentation
controller。

 1. 内嵌在同一个窗口中形式弹出新的ViewController open func presentViewControllerAsSheet(_ viewController: NSViewController) 2. 新窗口的形式弹出新的ViewController open func presentViewControllerAsModalWindow(_ viewController: NSViewController) 3. Popover的形式弹出新的ViewController open func presentViewController(_ viewController: NSViewController, asPopoverRelativeTo positioningRect: NSRect, of positioningView: NSView, preferredEdge: NSRectEdge, behavior: NSPopover.Behavior) 4. 从fromViewController转换到toViewController open func transition(from fromViewController: NSViewController, to toViewController: NSViewController, options: NSViewController.TransitionOptions = [], completionHandler completion:  -> Swift.Void)? = nil)

2.去我的简书下留言是最有效的交流方式,要加我好友就免了。

自定义呈现的过程

当呈现一个呈现样式为UIModalPresentationCustom的视图控制器时,UIKit会查看一个自定义presentation
controller来管理呈现过程。随着呈现的进行,UIKit会调用presentation
controller的方法,使其有机会设置任何自定义视图并将视图动画到某个位置。

presentation
controller与任何animator对象一起工作来实现整体转场。animator对象将视图控制器的内容动画显示到屏幕上,而presentation
controller处理所有其他事情。通常情况下,自定义presentation
controller会为其自己的视图创建动画。但是我们也可以覆写presentation
controller的presentedView方法,让animator对象为所有或者部分视图创建动画。

在呈现过程中,UIKit:

  1. 调用转场动画委托对象的presentationControllerForPresentedViewController:presentingViewController:sourceViewController:方法来检索我们自定义的presentation
    controller。
  2. 如果存在animator对象或者交互式animator对象的话,会向转场动画委托对象请求获取它们。
  3. 调用自定义presentation
    controller的presentationTransitionWillBegin方法。在该方法的实现中,应该将任何自定义视图添加到视图层次结构中,并为这些视图创建动画。
  4. 调用presentation
    controller的presentedView方法获取需要呈现的视图。presentedView方法返回的视图由animator对象为其创建动画。通常情况下,该方法返回需要呈现的视图控制器的根视图。自定义presentation
    controller可以根据需要来决定是否使用自定义背景视图来替换该视图。如果确实指定了不同的视图,则必须将需要呈现的视图控制器的根视图嵌入到presentation
    controller的视图层次结构中。
  5. 执行转场动画。转场动画包括animator对象创建的主要动画以及我们配置的与主要动画一起执行的任何动画。在动画过程中,UIKit会调用presentation
    controller的containerViewWillLayoutSubviewscontainerViewDidLayoutSubviews方法,以便我们可以根据需要调整自定义视图的布局。
  6. 在转场动画结束时,调用presentation
    controller的presentationTransitionDidEnd:方法。

在移除过程中,UIKit:

  1. 从呈现的视图控制器中获取我们自定义的presentation controller。
  2. 如果存在animator对象或者交互式animator对象的话,会向转场动画委托对象请求获取它们。
  3. 调用presentation
    controller的dismissalTransitionWillBegin方法。在该方法的实现中,应该将任何自定义视图添加到视图层次结构中,并为这些视图创建动画。
  4. 调用presentation controller的presentedView方法获取已经呈现的视图。
  5. 执行转场动画。转场动画包括animator对象创建的主要动画以及我们配置的与主要动画一起执行的任何动画。在动画过程中,UIKit会调用presentation
    controller的containerViewWillLayoutSubviewscontainerViewDidLayoutSubviews方法,以便我们能够删除任何自定义约束。
  6. 在转场动画结束时,调用presentation
    controller的dismissalTransitionDidEnd:方法。

在呈现过程中,presentation
controller的frameOfPresentedViewInContainerViewpresentedView方法可能会被多次调用,因此这两个方法的实现应该尽量简单。另外,在presentedView方法的实现中不应该尝试去设置视图层次结构。在调用该方法时,应该已经设置好视图层次结构。

在上面的系统提供的NSViewController四个方法中,可以分为presenttransition两种方式:

3.本文有一定的阅读门槛,并非适合新手的手把手入门教程,更适合照着教程写过几次转场动画过了几个月又忘了整个流程的人回顾学习精进。本文的结构以及相关知识点能让你回忆起当初亲手写出转场动画时的那股激动,除此之外,本文能满足你希望彻底搞懂转场的求知欲,我相信后者更重要,那种把分支技能树升满的感觉……

创建自定义presentation Controller

要实现自定义呈现样式,请子类化UIPresentationController并添加代码为自定义呈现创建视图和动画。创建自定义presentation
Controller时,请考虑以下问题:

  • 想添加哪些视图。
  • 想要屏幕上的视图执行怎样的动画效果。
  • 呈现的视图控制器的尺寸应该是多少。
  • 呈现的内容应该如何在水平正常和水平紧凑的屏幕环境之间进行适应。
  • 呈现完成后,是否移除发起呈现的视图控制器的视图。

所有这些决定都要求覆写UIPresentationController类的不同方法。

  • presentXXX: 所有的present方式都是通过调用
    presentViewController(NSViewController, animator: Animator)这个方法来完成展示的,并提供一个遵守NSViewControllerPresentationAnimator协议的animator控制整个动画过程.<如果希望实现自定义的Present转场效果,可以通过自定义animator方式后面会讲到具体实现步骤>

  • transition: 使用一个容器视图Contain View, 通过addSubView
    removeSubView的方式实现两个控制器之间的动画切换展示,系统提供了下面8中过渡动画方式:

4.怎么提问?新手如果觉得本文的范例啃不下去,去看源码,很简单。如果是关于转场动画中关键流程的地方,我相信本文已经做出了很好的解释,多读几遍;如果
Demo 里出了 Bug,请自己先确认好,然后在Demo
issue这里提交
issue
并给出你的详细测试环境;如果对本文中探讨机制以及缺陷的地方有疑问,欢迎留言交流。

设置呈现的视图控制器的Frame

可以修改呈现的视图控制器的frame,使其仅填充部分可用空间。默认情况下,呈现的视图控制器的尺寸能够完全填充容器视图。要更改frame,请覆写presentation
Controller的frameOfPresentedViewInContainerView方法。以下代码显示了一个示例,其中呈现的视图控制器的大小被改为仅覆盖容器视图的右半部分。在这种情况下,presentation
Controller使用背景调光视图来覆盖容器视图的另一半。

- (CGRect)frameOfPresentedViewInContainerView
{
    CGRect presentedViewFrame = CGRectZero;
    CGRect containerBounds = [[self containerView] bounds];

    presentedViewFrame.size = CGSizeMake(floorf(containerBounds.size.width / 2.0), containerBounds.size.height);

    presentedViewFrame.origin.x = containerBounds.size.width - presentedViewFrame.size.width;

    return presentedViewFrame;
}

前言

管理和动画自定义视图

自定义呈现样式通常涉及向呈现的内容中添加自定义视图。使用自定义视图来实现纯粹的视觉装饰或者使用它们将实际行为添加到呈现中。例如,背景视图可能包含手势识别器来跟踪呈现内容边界之外的特定操作。

presentation
Controller负责创建和管理与呈现有关的所有自定义视图。通常情况下,在presentation
Controller的初始化过程中创建自定义视图。以下代码显示了创建自己的调光视图的自定义视图控制器的初始化方法,此方法创建视图并执行一些最小配置。

- (instancetype)initWithPresentedViewController:(UIViewController *)presentedViewController presentingViewController:(UIViewController *)presentingViewController
{
    self = [super initWithPresentedViewController:presentedViewController presentingViewController:presentingViewController];

    if(self)
    {
        // Create the dimming view and set its initial appearance.
        self.dimmingView = [[UIView alloc] init];

        [self.dimmingView setBackgroundColor:[UIColor colorWithWhite:0.0 alpha 0.4]];

        [self.dimmingView setAlpha:0.0];
    }
    return self;
}

使用presentationTransitionWillBegin方法将自定义视图动画显示到屏幕上。在此方法的实现中,配置自定义视图并将其添加到容器视图中,如下代码所示。使用发起呈现或者呈现的视图控制器的转场动画协调器来创建任何动画。切勿在此方法中修改呈现的视图控制器的视图。animator对象负责将呈现的视图控制器动画显示到frameOfPresentedViewInContainerView方法返回的frame去。

- (void)presentationTransitionWillBegin
{
    // Get critical information about the presentation.
    UIView* containerView = [self containerView];

    UIViewController* presentedViewController = [self presentedViewController];

    // Set the dimming view to the size of the container's
    // bounds, and make it transparent initially.
    [[self dimmingView] setFrame:[containerView bounds]];
    [[self dimmingView] setAlpha:0.0];

    // Insert the dimming view below everything else.
    [containerView insertSubview:[self dimmingView] atIndex:0];

    // Set up the animations for fading in the dimming view.
    if([presentedViewController transitionCoordinator])
    {
        [[presentedViewController transitionCoordinator] animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext>context){
            // Fade in the dimming view.
            [[self dimmingView] setAlpha:1.0];
        } completion:nil];
    }else
    {
        [[self dimmingView] setAlpha:1.0];
    }
}

在呈现结束时,使用presentationTransitionDidEnd:方法来处理由于取消呈现所导致的任何清理。如果不满足阀值条件,交互式animator对象可能会取消转场动画。发生这种情况时,UIKit会调用presentationTransitionDidEnd:方法并传递NO值给该方法。当发生取消转场动画操作时,删除在呈现开始时添加的任何自定义视图,并将其他视图还原为之前的配置,如下所示。

- (void)presentationTransitionDidEnd:(BOOL)completed
{
    // If the presentation was canceled, remove the dimming view.
    if (!completed)
        [self.dimmingView removeFromSuperview];
}

当视图控制器被移除时,使用dismissalTransitionDidEnd:方法从视图层次结构中删除自定义视图。如果想要视图动画消失,请在dismissalTransitionDidEnd:方法中配置这些动画。以下代码显示了在前面的例子中移除调光视图的两种方法的实现。始终检查dismissalTransitionDidEnd:方法的参数以确定移除是成功还是被取消。

- (void)dismissalTransitionWillBegin
{
    // Fade the dimming view back out.
    if([[self presentedViewController] transitionCoordinator])
    {
        [[[self presentedViewController] transitionCoordinator] animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext>context) {
            [[self dimmingView] setAlpha:0.0];
        } completion:nil];
    }else
    {
        [[self dimmingView] setAlpha:0.0];
    }
}

- (void)dismissalTransitionDidEnd:(BOOL)completed
{
    // If the dismissal was successful, remove the dimming view.
    if (completed)
        [self.dimmingView removeFromSuperview];
}
 @available(OSX 10.10, *) public struct TransitionOptions : OptionSet { public static var crossfade: NSViewController.TransitionOptions { get } public static var slideUp: NSViewController.TransitionOptions { get } public static var slideDown: NSViewController.TransitionOptions { get } public static var slideLeft: NSViewController.TransitionOptions { get } public static var slideRight: NSViewController.TransitionOptions { get } public static var slideForward: NSViewController.TransitionOptions { get } public static var slideBackward: NSViewController.TransitionOptions { get } public static var allowUserInteraction: NSViewController.TransitionOptions { get } }

屏幕左边缘右滑返回,TabBar
滑动切换,你是否喜欢并十分依赖这两个操作,甚至觉得 App
不支持这类操作的话简直反人类?这两个操作在大屏时代极大提升了操作效率,其背后的技术便是今天的主题:视图控制器转换(View
Controller Transition)。

传递presentation Controller给UIKit

呈现一个视图控制器时,请执行以下操作来使用自定义presentation
Controller显示视图控制器:

  • 将需要呈现的视图控制器的UIModalPresentationCustom属性设置为UIModalPresentationCustom
  • 给需要呈现的视图控制器的transitioningDelegate属性分配一个转场动画委托对象。
  • 实现转场动画委托对象的presentationControllerForPresentedViewController:presentingViewController:sourceViewController:方法。

当UIKit需要使用自定义presentation
Controller时,会调用转场动画委托对象的presentationControllerForPresentedViewController:presentingViewController:sourceViewController:方法。该方法的实现应该以下代码那样简单,只需要创建自定义的presentation
Controller,进行配置并返回。如果此方法返回nil,则UIKit会使用全屏呈现样式来呈现视图控制器。

- (UIPresentationController *)presentationControllerForPresentedViewController:
(UIViewController *)presented presentingViewController:(UIViewController *)presenting sourceViewController:(UIViewController *)source
{
    MyPresentationController* myPresentation = [[MyPresentationController] initWithPresentedViewController:presented presentingViewController:presenting];

    return myPresentation;
}
crossfade 效果

ca88会员登录中心 2crossfade效果

视图控制器中的视图显示在屏幕上有两种方式:最主要的方式是内嵌在容器控制器中,比如
UINavigationController,UITabBarController,
UISplitController;由另外一个视图控制器显示它,这种方式通常被称为模态(Modal)显示。View
Controller Transition 是什么?在 NavigationController 里 push 或 pop
一个 View Controller,在 TabBarController 中切换到其他 View
Controller,以 Modal 方式显示另外一个 View Controller,这些都是 View
Controller Transition。在 storyboard 里,每个 View Controller 是一个
Scene,View Controller Transition 便是从一个 Scene 转换到另外一个
Scene;为方便,以下对 View Controller Transition 的中文称呼采用
Objccn.io 中的翻译「转场」。

适应不同的屏幕环境

在屏幕上呈现内容时,UIKit会在底层特征发生改变或者容器视图的尺寸发生变化时通知我们自定义的presentation
Controller。这种变化通常发生在设备旋转过程中,但也可能发生在其他时间。可以使用trait和size通知来适当调整presentation
Controller的自定义视图并更新为合适的呈现样式。

有关如何适应新的trait和size的信息,请参看Building an Adaptive
Interface。

slideUp/slideDown 效果

ca88会员登录中心 3slideUp/slideDown
效果

在 iOS 7
之前,我们只能使用系统提供的转场效果,大部分时候够用,但仅仅是够用而已,总归会有各种不如意的小地方,但我们却无力改变;iOS
7 开放了相关 API
允许我们对转场效果进行全面定制,这太棒了,转场配合动画以及对交互手段的支持带来了无限可能,像开头提到的两种转场搭配简单的动画带来了便利的交互操作,有些转场配合华丽的动画则能让转场变得赏心悦目。

Demo

Demo地址:https://github.com/zhangshijian/UIViewControllerDemo

slideLeft/slideRight (slideForward/slideBackward ) 效果

ca88会员登录中心 4slideLeft/slideRight
(slideForward/slideBackward ) 效果

我知道你更想知道如何实现好看的转场动画,不过本文并非华丽的转场动画教程,相反,文中的转场动画效果都十分简单,但我会教你彻底掌握转场动画中转场的那部分,包括转场背后的机制,缺陷以及实现过程中的技巧与陷阱。阅读本文需要读者至少要对
ViewController 和 View
的结构以及协议有基本的了解,最好自己亲手实现过一两种转场动画。如果你对此感觉没有信心,推荐观看官方文档:View
Controller Programming Guide for
iOS,学习此文档将会让你更容易理解本文的内容。对你想学习的小节,我希望你自己亲手写下这些代码,一步步地看着效果是如何实现的,至少对我而言,看各种相关资料时只有字面意义上的理解,正是一步步的试验才能让我理解每一个步骤。本文涉及的内容较多,为了避免篇幅过长,我只给出关键代码而不是从新建工程开始教你每一个步骤。本文基于
Xcode 7 以及 Swift 2,Demo
合集地址:iOS-ViewController-Transition-Demo。

allowUserInteraction 效果

ca88会员登录中心 5allowUserInteraction
效果

在进行transition时,所有需要切换的child ViewController必须是同一个
super ViewController,否则会抛出异常错误.

  1. transition方法仅支持有父子关系的控制器结构.
  2. transition由父控制器super ViewController进行调用.
  3. transition仅在子控制器child ViewController之间进行切换.
  4. transition方法中,fromViewcontroller
    的视图必须有superView,否则抛出异常.

示例代码: TransAnimationController demo

  1. 搭建UI界面:

ca88会员登录中心 6构建UI界面

  1. 代码部分:

class ViewController: NSViewController { 1. 从Storyboard中的CustomView 连线的控件属性,用来作为容器视图,显示每个ChildViewController的内容 @IBOutlet weak var containView: MYContainView! override func viewDidLoad() { super.viewDidLoad() 2. 添加需要切换的子控制器: RedController 和BlueController 为自定义的两个控制器,仅显示不同的视图颜色. addChildViewController(RedController addChildViewController(BlueController3. 需要将第一个ChildViewController的view添加到容器视图中; containView.addSubview(childViewControllers[0].view)4. 设置容器视图的颜色 containView.layer?.backgroundColor = NSColor.orange.cgColor }5. 点击下一个按钮, 从RedController 切换到BlueController @IBAction func clickBtn(_ sender: Any) { transition(from: childViewControllers[0], to: childViewControllers[1], options: .slideLeft, completionHandler: nil) }6. 点击上一个按钮, 从BlueController 切换到RedController @IBAction func clickUpButton(_ sender: Any) { transition(from: childViewControllers[1], to: childViewControllers[0], options: .slideRight, completionHandler: nil) }}6. 修改4,5 步骤中的option 参数,可以实现不同的transition 效果.
  • presentViewControllerAsSheet

     @IBAction func presentTest(_ sender: Any) { 1. 创建控制器 let greenVC = GreenController() 2. 以AsSheet方式弹出控制器 presentViewControllerAsSheet }
    

ca88会员登录中心 7AsSheet

  • presentViewControllerAsModalWindow

 @IBAction func presentTest(_ sender: Any) { let greenVC = GreenController() 1. 以AsSheet方式弹出控制器 presentViewControllerAsModalWindow }

ca88会员登录中心 8AsModalWindow

  • presentViewControllerAsPopover

 let greenVC = GreenController() 1. 以Popover方式弹出控制器 presentViewController(greenVC, asPopoverRelativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX, behavior: NSPopover.Behavior.transient)

ca88会员登录中心 9Jul-28-2018
20-56-14.gif

  1. 自定义一个遵守NSViewControllerPresentationAnimator 协议的对象
  2. 实现NSViewControllerPresentationAnimator的两个方法

public protocol NSViewControllerPresentationAnimator : NSObjectProtocol {1. present 动画时,执行这个方法,因此在这个方法中实现自定义的动画效果 public func animatePresentation(of viewController: NSViewController, from fromViewController: NSViewController)2. dismiss动画时,执行这个方法 ,在这个方法中可以实在自定义的动画效果 public func animateDismissal(of viewController: NSViewController, from fromViewController: NSViewController)}
  1. 在需要执行Present的地方调用presentViewController(ViewController, animator: )
  • 示例代码: TransAnimationController demo

class PresentAnimator: NSObject { }// MARK: NSViewControllerPresentationAnimatorextension PresentAnimator: NSViewControllerPresentationAnimator{ func animatePresentation(of viewController: NSViewController, from fromViewController: NSViewController) { // 这里实现present的动画效果 /**viewController: 将要被present出来的视图控制器, fromViewcontroller --> presented动作 ---> viewController */ 1. 获取容器view let containerView = fromViewController.view 2. 计算最终显示的frame let finalFrame = NSInsetRect(containerView.bounds, 50, 50) 3. 需要显示的view let modalView = viewController.view 4. 设置将要显示视图的初始frame modalView.frame = finalFrame modalView.setFrameOrigin(NSMakePoint(finalFrame.origin.x, finalFrame.origin.y - 200)) 5 .添加视图到容器视图中 containerView.addSubview(modalView) 6. 执行动画效果 NSAnimationContext.runAnimationGroup({ (animationContext) in animationContext.duration = 0.5 modalView.animator().frame = finalFrame }, completionHandler: nil) } func animateDismissal(of viewController: NSViewController, from fromViewController: NSViewController) { // 这里实现dismiss时的动画效果 1. 获取开始动画的frame let startFrame = viewController.view.frame 2. 执行动画 NSAnimationContext.runAnimationGroup({ (animationContext) in animationContext.duration = 0.5 viewController.view.animator().setFrameOrigin(NSMakePoint(startFrame.origin.x, startFrame.origin.y - fromViewController.view.bounds.size.height - 100)) }) { 3. 动画完成后,移除子视图 viewController.view.removeFromSuperview() } }}
  • 示例效果:

ca88会员登录中心 10自定义present
动画效果

  1. macOS中,控制器的转场切换无论是presentViewController方式或者transition方式,本质上都是将要显示的控制器视图View,通过addSubView方法添加到容器视图中展示.

  2. ca88会员登录中心,通常开发中如果没有特殊需求,transition的系统样式基本都可以满足使用.

  3. 自定义present 动画时,需要注意事件穿透问题:

    • 由于显示出来的控制器视图(Controller View)是通过addSubView方式添加到容器视图中,因此在控制器视图(Controller View)上进行点击操作,可能会触发容器视图中控件的方法

    • 解决办法:
      给容器视图添加一层背景视图(自定义的NSView, 重写mouseDown方法即可),通过背景视图屏蔽鼠标操作,防止事件穿透到容器视图中

为了方便更多同学可以了解到macOS开发相关的内容,我准备授权同步到腾讯云+社区上,希望大家多多支持…我的博客即将搬运同步至腾讯云+社区,邀请大家一同入驻:

文章越来越长了,分成三个部分:

第一部分:

前奏:触发转场的方式

Transition
解释

阶段一:非交互转场

动画控制器协议

动画控制器实现

特殊的 Modal
转场

Modal
转场的差异

Modal
转场实践

iOS 8
的改进:UIPresentationController

转场代理

第二部分:PartII
Link(以下目录无法跳转,请点击该链接查看内容)

阶段二:交互式转场

实现交互化

Transition
Coordinator

特殊的 Modal
转场交互化

封装交互控制器

交互转场的限制

iOS 10 新特性:
全程交互控制

第三部分:PartIII
Link(以下目录无法跳转,请点击该链接查看内容)

插曲:UICollectionViewController
布局转场

进阶

案例分析

子元素动画

Mask
动画

高性能动画框架

自定义容器控制器转场

实现分析

协议补完

交互控制

动画控制和 CAMediaTiming
协议

取消转场

最后的封装

尾声:转场动画的设计

版权申明以及其他

前奏:触发转场的方式

目前为止,官方支持以下几种方式的自定义转场:

在 UINavigationController 中 push 和 pop;

在 UITabBarController 中切换 Tab;

Modal 转场:presentation 和
dismissal,俗称视图控制器的模态显示和消失,仅限于modalPresentationStyle属性为
UIModalPresentationFullScreen 或 UIModalPresentationCustom 这两种模式;

UICollectionViewController 的布局转场:仅限于 UICollectionViewController
与 UINavigationController
结合的转场方式,与上面三种都有点不同,不过实现很简单,可跳转至该链接查看。

官方的支持包含了 iOS
中的大部分转场方式,还有一种自定义容器中的转场并没有得到系统的直接支持,不过借助协议这种灵活的方式,我们依然能够实现对自定义容器控制器转场的定制,在压轴环节我们将实现这一点。

以上前三种转场都需要转场代理和动画控制器(见下节)的帮助才能实现自定义转场动画,而触发的方式分为三种:代码里调用相关动作的方法,Segue
以及,对于上面两种容器 VC,在 UINavigationBar 和 UITabBar 上的相关 Item
的点击操作。

相关动作方法

UINavigationController 中所有修改其viewControllers栈中 VC
的方法都可以自定义转场动画:

//我们使用的最广泛的 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)

UITabBarController 下没什么特别的:

//注意传递的参数必须是其下的子 VC

unowned(unsafe) var selectedViewController: UIViewController?

var selectedIndex: Int

//和上面类似的整体更新

func setViewControllers(_ viewControllers: [UIViewController]?,
animated animated: Bool)

Modal 转场:

// Presentation 转场

func presentViewController(_ viewControllerToPresent: UIViewController,
animated flag: Bool, completion completion: (() -> Void)?)

// Dismissal 转场

func dismissViewControllerAnimated(_ flag: Bool, completion completion:
(() -> Void)?)

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 引入了适应性布局,由此添加了两种新的方式来显示一个视图控制器:

func showViewController(_ vc: UIViewController, sender sender:
AnyObject?)

func showDetailViewController(_ vc: UIViewController, sender sender:
AnyObject?)

这两个方法咋看上去是给 UISplitViewController 用的,在 storyboard 里
segue 的候选模式里,直接给出了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)

Transition
解释

前言里从行为上解释了转场,那在转场时发生了什么?下图是从 WWDC 2013
Session 218 整理的,解释了转场时视图控制器和其对应的视图在结构上的变化:

转场过程中,作为容器的父 VC 维护着多个子
VC,但在视图结构上,只保留一个子 VC 的视图,所以转场的本质是下一场景(子
VC)的视图替换当前场景(子 VC)的视图以及相应的控制器(子
VC)的替换,表现为当前视图消失和下一视图出现,基于此进行动画,动画的方式非常多,所以限制最终呈现的效果就只有你的想象力了。图中的
Parent VC 可替换为 UIViewController, UITabbarController 或
UINavigationController 中的任何一种。

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

1.转场代理(Transition Delegate):

自定义转场的第一步便是提供转场代理,告诉系统使用我们提供的代理而不是系统的默认代理来执行转场。有如下三种转场代理,对应上面三种类型的转场:

//UINavigationController 的 delegate 属性遵守该协议。

//UITabBarController 的 delegate 属性遵守该协议。

//UIViewController 的 transitioningDelegate 属性遵守该协议。

这里除了是
iOS 7 新增的协议,其他两种在 iOS 2 里就存在了,在 iOS 7
时扩充了这两种协议来支持自定义转场。

转场发生时,UIKit
将要求转场代理将提供转场动画的核心构件:动画控制器和交互控制器(可选的);由我们实现。

2.动画控制器(Animation Controller):

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

3.交互控制器(Interaction Controller):

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

4.转场环境(Transition Context):

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

5.转场协调器(Transition Coordinator):

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

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

阶段一:非交互转场

这个阶段要做两件事,提供转场代理并由代理提供动画控制器。在转场代理协议里动画控制器和交互控制器都是可选实现的,没有实现或者返回
nil
的话则使用默认的转场效果。动画控制器是表现转场效果的核心部分,代理部分非常简单,我们先搞定动画控制器吧。

动画控制器协议

动画控制器负责添加视图以及执行动画,遵守UIViewControllerAnimatedTransitioning协议,该协议要求实现以下方法:

//执行动画的地方,最核心的方法。

(Required)func animateTransition(_ transitionContext:
UIViewControllerContextTransitioning)

//返回动画时间,”return 0.5″
已足够,非常简单,出于篇幅考虑不贴出这个方法的代码实现。

(Required)func transitionDuration(_ transitionContext:
UIViewControllerContextTransitioning?) -> NSTimeInterval

//如果实现了,会在转场动画结束后调用,可以执行一些收尾工作。

(Optional)func animationEnded(_ transitionCompleted: Bool)

最重要的是第一个方法,该方法接受一个遵守协议的转场环境对象,上一节的 API
解释里提到这个协议,它提供了转场所需要的重要数据:参与转场的视图控制器和转场过程的状态信息。

UIKit 在转场开始前生成遵守转场环境协议的对象
transitionContext,它有以下几个方法来提供动画控制器需要的信息:

//返回容器视图,转场动画发生的地方。

func containerView() -> UIView?

//获取参与转场的视图控制器,有 UITransitionContextFromViewControllerKey
和 UITransitionContextToViewControllerKey 两个 Key。

func viewControllerForKey(_ key: String) -> UIViewController?

//iOS 8新增 API 用于方便获取参与参与转场的视图,有
UITransitionContextFromViewKey 和 UITransitionContextToViewKey 两个
Key。

func viewForKey(_ key: String) -> UIView? AVAILABLE_IOS(8_0)

通过viewForKey:获取的视图是viewControllerForKey:返回的控制器的根视图,或者
nil。viewForKey:方法返回 nil 只有一种情况: UIModalPresentationCustom
模式下的 Modal 转场 ,通过此方法获取 presentingView 时得到的将是
nil,在后面的 Modal 转场里会详细解释。

前面提到转场的本质是下一个场景的视图替换当前场景的视图,从当前场景过渡下一个场景。下面称即将消失的场景的视图为
fromView,对应的视图控制器为 fromVC,即将出现的视图为
toView,对应的视图控制器称之为
toVC。几种转场方式的转场操作都是可逆的,一种操作里的 fromView 和 toView
在逆向操作里的角色互换成对方,fromVC 和 toVC
也是如此。在动画控制器里,参与转场的视图只有 fromView 和 toView
之分,与转场方式无关。你可以在 fromView 和 toView
上添加任何动画,转场动画的最终效果只限制于你的想象力。
这也是动画控制器在封装后可以被第三方使用的重要原因。

在 iOS 8 中可通过以下方法来获取参与转场的三个重要视图,在 iOS 7
中则需要通过对应的视图控制器来获取,为避免 API
差异导致代码过长,示例代码中直接使用下面的视图变量:

let containerView = transitionContext.containerView()

let fromView =
transitionContext.viewForKey(UITransitionContextFromViewKey)

let toView = transitionContext.viewForKey(UITransitionContextToViewKey)

动画控制器实现

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

需要举个例子了,实现哪个好呢?

毫无疑问,上面那个简单的。Are you kidding
me?这种转场动画也需要你写这么长的废话来教我怎么实现?好吧,你要知道转场动画是转场与动画的配合,下面更炫酷一点的转场动画和上面的五毛动画相比,它们在转场技术部分并没有什么区别,主要的差别在动画的部分。事实是,不管复杂与否,所有的转场动画在实现转场的部分都没有什么差别,而且从技术上来讲,实现转场并没有高深的东西,如果你动手实现过几次,你就能搞定所有的转场动画中转场的那部分。所以,为了安安静静学习转场以及省点篇幅,我选择上面的转场动画作为例子。

在交互式转场章节里我们将在上面 Slide
动画的基础上实现文章开头提到的两种效果:NavigationController 右滑返回 和
TabBarController
滑动切换。尽管对动画控制器来说,转场方式并不重要,可以对 fromView 和
toView 进行任何动画,是的,任何动画,但上面的动画和 Modal
转场风格上有点不配,主要动画的方向不对,我在这个 Slide 动画控制器里为
Modal 转场适配了和系统的风格类似的竖直移动动画效果;另外 Modal
转场并没有比较合乎操作直觉的交互手段,而且和前面两种容器控制器的转场在机制上有些不同,所以我将为
Modal 转场示范另外一个动画。

Demo 中的 Slide
动画控制器适用于三种转场,不必修改就可以直接在工程中使用。转场中的操作是可逆的,你可以为了每一种操作实现单独的动画控制器,也可以实现通用的动画控制器。为此,Demo
中的 Slide 动画控制器针对转场的操作类型进行了适配。Swift 中 enum
的关联值可以视作有限数据类型的集合体,在这种场景下极其合适。设定转场类型:

enum SDETransitionType{

//UINavigationControllerOperation 是枚举类型,有 None, Push, Pop
三种值。

case NavigationTransition(UINavigationControllerOperation)

case TabTransition(TabOperationDirection)

case ModalTransition(ModalOperation)

}

enum TabOperationDirection{

case Left, Right

}

enum ModalOperation{

case Presentation, Dismissal

}

使用示例:在 TabBarController 中切换到左边的页面。

let transitionType = SDETransitionType.TabTransition(.Left)

Slide 动画控制器的核心代码:

class SlideAnimationController: NSObject,
UIViewControllerAnimatedTransitioning {

init(type: SDETransitionType) {…}

func animateTransition(transitionContext:
UIViewControllerContextTransitioning) {

//1

containerView.addSubview(toView)

//计算位移 transform,NavigationVC 和 TabBarVC 在水平方向进行动画,Modal
转场在竖直方向进行动画。

var toViewTransform = …

var fromViewTransform = …

toView.transform = toViewTransform

//根据协议中的方法获取动画的时间。

let duration = self.transitionDuration(transitionContext)

UIView.animateWithDuration(duration, animations: {

fromView.transform = fromViewTransform

toView.transform = CGAffineTransformIdentity

}, completion: { _ in

//考虑到转场中途可能取消的情况,转场结束后,恢复视图状态。

fromView.transform = CGAffineTransformIdentity

toView.transform = CGAffineTransformIdentity

//2

let isCancelled = transitionContext.transitionWasCancelled()

transitionContext.completeTransition(!isCancelled)

})

}

}

注意上面的代码有2处标记,是动画控制器必须完成的:

将 toView 添加到容器视图中,使得 toView 在屏幕上显示( Modal
转场中此点稍有不同,下一节细述),也不必非得是addSubview:,某些场合你可能需要调整
fromView 和 toView 的显示顺序,总之将之加入到 containerView 里就行了;

动画结束后正确地结束转场过程。转场的结果有两种:完成或取消。非交互转场的结果只有完成一种情况,不过交互式转场需要考虑取消的情况。如何结束取决于转场的进度,通过transitionWasCancelled()方法来获取转场的结果,然后使用completeTransition:来通知系统转场过程结束,这个方法会检查动画控制器是否实现了animationEnded:方法,如果有,则调用该方法。

至此,你已经能够搞定任何动画控制器中转场的部分了,无论转场动画是简单的还是超级复杂的,是的,就这么简单,没有任何高深的东西了。转场结束后,fromView
会从视图结构中移除,UIKit 自动替我们做了这事,你也可以手动处理提前将
fromView
移除,这完全取决于你。虽然这个动画控制器实现的动画非常简单,但此刻我们已经替换掉了系统提供的默认转场动画。

以上的代码是常规的实现手法,这里还有另外一条更简单的路:UIView的类方法

transitionFromView:toView:duration:options:completion:

甚至不需要获取 containerView 以及手动添加 toView
就能实现一个指定类型的转场动画,而缺点则是只能使用指定类型的动画。

UIView.transitionFromView(fromView, toView: toView, duration: durantion,
options: .TransitionCurlDown, completion: { _ in

let isCancelled = transitionContext.transitionWasCancelled()

transitionContext.completeTransition(!isCancelled)

})

看到这里是否想起了点什么?UIViewController用于在子 VC 间转换的方法:

transitionFromViewController:toViewController:duration:options:animations:completion:

该方法用 toVC 的视图替换 fromVC
的视图在父视图中的位置并且执行animations闭包里的动画,但这个方法仅限于在自定义容器控制器里使用,直接使用
UINavigationController 和 UITabBarController 调用该方法在其下的子 VC
间转换会抛出异常。不过 iOS 7
中这两个容器控制器开放的自定义转场做的是同样的事情,回头再看第一章Transition
解释,转场协议
API
将这个方法拆分成了上面的几个组件,并且加入了激动人心的交互控制,以便我们能够方便定制转场动画。

特殊的 Modal
转场

Modal
转场的差异

事先声明:尽管 Modal 转场和上面两种容器 VC
的转场在控制器结构以及视图结构都有点差别,但是在代码里实现转场时,差异非常小,仅有一处地方需要注意。所以,本节也可以直奔末尾,记住结论就好。

上一节里两种容器 VC 的转场里,fromVC 和 toVC 都是其子 VC,而在 Modal
转场里并非这样的关系,fromVC(presentingVC) present
toVC(presentedVC),前者为后者提供显示的环境。两类转场的视图结构差异如下:

转场前后可以在控制台打印出它们的视图控制器结构以及视图结构观察变化情况,不熟悉相关命令的话推荐使用chisel工具,而使用
Xcode 的 ViewDebugging 功能可以直观地查看应用的视图结构。如果你对转场中
containerView 这个角色感兴趣,可以通过上面的方法来查看。

容器类 VC 的转场里 fromView 和 toView 是 containerView
的子层次的视图,而 Modal 转场里 presentingView 与 containerView
是同层次的视图,只有 presentedView 是 containerView 的子层次视图。

这种视图结构上的差异与 Modal 转场的另外一个不同点是相契合的:转场结束后
fromView 可能依然可见,比如 UIModalPresentationPageSheet 模式的 Modal
转场就是这样。容器 VC 的转场结束后 fromView
会被主动移出视图结构,这是可预见的结果,我们也可以在转场结束前手动移除;而
Modal 转场中,presentation 结束后 presentingView(fromView)
并未主动被从视图结构中移除。准确来说,在我们可自定义的两种模式里,UIModalPresentationCustom
模式(以下简称 Custom 模式)下 Modal 转场结束时 fromView
并未从视图结构中移除;UIModalPresentationFullScreen 模式(以下简称
FullScreen 模式)的 Modal 转场结束后 fromView
依然主动被从视图结构中移除了。这种差异导致在处理 dismissal
转场的时候很容易出现问题,没有意识到这个不同点的话出错时就会毫无头绪。

来看看 dismissal 转场时的场景:

FullScreen 模式:presentation 结束后,presentingView
被主动移出视图结构,不过,在 dismissal
转场中希望其出现在屏幕上并且在对其添加动画怎么办呢?实际上,你按照容器类
VC 转场里动画控制器里那样做也没有问题,就是将其加入 containerView
并添加动画。不用担心,转场结束后,UIKit
会自动将其恢复到原来的位置。虽然背后的机制不一样,但这个模式下的 Modal
转场和容器类 VC 的转场的动画控制器的代码可以通用,你不必记住背后的差异。

Custom 模式:presentation 结束后,presentingView(fromView)
未被主动移出视图结构,在 dismissal 中,注意不要像其他转场中那样将
presentingView(toView) 加入 containerView 中,否则 dismissal
结束后本来可见的 presentingView 将会随着 containerView
一起被移除。如果你在 Custom
模式下没有注意到这点,很容易出现黑屏之类的现象而不知道问题所在。

对于 Custom 模式,我们可以参照其他转场里的处理规则来打理:presentation
转场结束前手动将 fromView(presentingView)
移出它的视图结构,并用一个变量来维护 presentingView 的父视图,以便在
dismissal 转场中恢复;在 dismissal 转场中,presentingView 的角色由原来的
fromView 切换成了
toView,我们再将其重新恢复它原来的视图结构中。测试表明这样做是可行的。但是这样一来,在实现上,需要动画控制器用一个变量来保存
presentingView 的父视图以便在 dismissal
转场中恢复,第三方的动画控制器必须为此改造。显然,这样的代价是无法接受的。为何
FullScreen 模式的 dismissal 转场里就可以任性地将 presentingView 加入到
containerView 里呢?因为 UIKit 知道 presentingView
的视图结构,即使强行将其从原来的视图结构迁移到
containerView,事后将其恢复到正确的位置也是很容易的事情。

由于以上的区别导致实现交互化的时候在 Custom
模式下无法控制转场过程中添加到 presentingView
上面的动画。解决手段请看特殊的 Modal
转场交互化一节。

结论:不要干涉官方对 Modal 转场的处理,我们去适应它。在 Custom
模式下的 dismissal 转场中不要像其他的转场那样将 toView(presentingView)
加入 containerView,否则 presentingView
将消失不见,而应用则也很可能假死。而 FullScreen
模式下可以使用与前面的容器类 VC 转场同样的代码
。因此,上一节里示范的
Slide 动画控制器不适合在 Custom 模式下使用,放心好了,Demo
里适配好了,具体的处理措施,请看下一节的处理。

iOS 8 为协议添加了viewForKey:方法以方便获取 fromView 和 toView,但是在
Modal 转场里要注意,presentingView 并非 containerView
的子视图,这时通过viewForKey:方法来获取 presentingView 得到的是
nil,必须通过viewControllerForKey:得到 presentingVC 后来获取。因此在
Modal 转场中,较稳妥的方法是从 fromVC 和 toVC 中获取 fromView 和
toView。

Modal
转场实践

UIKit 已经为 Modal 转场实现了多种效果,当 UIViewController
的modalPresentationStyle属性为.Custom或.FullScreen时,我们就有机会定制转场效果,此时modalTransitionStyle指定的转场动画将会被忽略。补充说明:自定义
Modal
转场时,modalPresentationStyle属性也可以为其他值,当你提供了转场代理和动画控制器后,系统就将转场这件事全权交给你负责了,UIKit
内部并没有对modalPresentationStyle的值进行过滤,然而该属性的值不是.Custom或.FullScreen这两个官方支持的值时,会出现各种瑕疵。总之,在探索时可以各种试探,但是干活时还是老老实实听官方的话。详细讨论可以查看这个issue。

Modal 转场开放自定义功能后最令人感兴趣的是定制 presentedView
的尺寸,下面来我们来实现一个带暗色调背景的小窗口效果。Demo
地址:CustomModalTransition。

由于需要保持 presentingView 可见,这里的 Modal 转场应该采用
UIModalPresentationCustom 模式,此时 presentedVC
的modalPresentationStyle属性值应设置为.Custom。而且与容器 VC
的转场的代理由容器 VC 自身的代理提供不同,Modal 转场的代理由 presentedVC
提供。动画控制器的核心代码:

class OverlayAnimationController: NSobject,
UIViewControllerAnimatedTransitioning{

func animateTransition(transitionContext:
UIViewControllerContextTransitioning) {

//不像容器 VC 转场里需要额外的变量来标记操作类型,UIViewController
自身就有方法跟踪 Modal 状态。

//处理 Presentation 转场:

if toVC.isBeingPresented(){

//1

containerView.addSubview(toView)

//在 presentedView 后面添加暗背景视图 dimmingView,注意两者在
containerView 中的位置。

let dimmingView = UIView()

containerView.insertSubview(dimmingView, belowSubview: toView)

//设置 presentedView 和 暗背景视图 dimmingView 的初始位置和尺寸。

let toViewWidth = containerView.frame.width * 2 / 3

let toViewHeight = containerView.frame.height * 2 / 3

toView.center = containerView.center

toView.bounds = CGRect(x: 0, y: 0, width: 1, height: toViewHeight)

dimmingView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)

dimmingView.center = containerView.center

dimmingView.bounds = CGRect(x: 0, y: 0, width: toViewWidth, height:
toViewHeight)

//实现出现时的尺寸变化的动画:

UIView.animateWithDuration(duration, delay: 0, options: .CurveEaseInOut,
animations: {

toView.bounds = CGRect(x: 0, y: 0, width: toViewWidth, height:
toViewHeight)

dimmingView.bounds = containerView.bounds

}, completion: {_ in

//2

let isCancelled = transitionContext.transitionWasCancelled()

transitionContext.completeTransition(!isCancelled)

})

}

//处理 Dismissal 转场,按照上一小节的结论,.Custom 模式下不要将 toView
添加到 containerView,省去了上面标记1处的操作;

if fromVC.isBeingDismissed(){

let fromViewHeight = fromView.frame.height

UIView.animateWithDuration(duration, animations: {

fromView.bounds = CGRect(x: 0, y: 0, width: 1, height: fromViewHeight)

}, completion: { _ in

//2

let isCancelled = transitionContext.transitionWasCancelled()

transitionContext.completeTransition(!isCancelled)

})

}

}

}

Modal 转场在 Custom 模式下必须区分 presentation 和 dismissal 转场,而在
FullScreen 模式下可以不用这么做,因为 UIKit 会在 dismissal
转场结束后自动将 presentingView 放置到原来的位置。

在 Demo 里,Slide 动画控制器里适配所有类型的转场是这样处理的:

switch transitionType{

case .ModalTransition(let operation):

switch operation{

case .Presentation: containerView.addSubview(toView)

case .Dismissal: break

}

default: containerView.addSubview(toView)

}

转场环境对象本身也提供了presentationStyle()方法来查询 Modal
转场的类型,在一般通用型的动画控制器里可以这样处理:

if !(transitionContext.presentationStyle() == .Custom &&
fromVC.isBeingDismissed()){

containerView.addSubview(toView)

}

前面容器 VC
的转场里提到可以使用UIView的类方法transitionFromView:toView:duration:options:completion:在animateTransition:方法中来执行子视图的转换,Modal
转场里,fromView 和 toView
并非同一容器视图下同层次的子视图,该方法并不适用。不过经测试,该方法在
Custom 模式下工作正常,FullScreen 模式有点不兼容。由于在 Modal
转场支持两种模式,为避免混淆建议不要使用该方法来转换视图。

至此,三种主流转场的动画控制器基本介绍完毕了,可以看到动画控制器里有关转场的部分是非常简单的,没什么难度,也没什么高级的用法,剩下的动画部分,如前面提到的那样,你可以为
fromView 和 toView 添加任何动画,而这又是另外一个话题了。

iOS
8的改进:UIPresentationController

iOS 8 针对分辨率日益分裂的 iOS
设备带来了新的适应性布局方案,以往有些专为在 iPad 上设计的控制器也能在
iPhone
上使用了,一个大变化是在视图控制器的(模态)显示过程,包括转场过程,引入了UIPresentationController类,该类接管了
UIViewController 的显示过程,为其提供转场和视图管理支持。在 iOS 8.0
以上的系统里,你可以在 presentation
转场结束后打印视图控制器的结构,会发现 presentedVC
是由一个UIPresentationController对象来显示的,查看视图结构也能看到
presentedView 是 UIView 私有子类的UITtansitionView的子视图,这就是前面
containerView 的真面目(剧透了)。

当 UIViewController
的modalPresentationStyle属性为.Custom时(不支持.FullScreen),我们有机会通过控制器的转场代理提供UIPresentationController的子类对
Modal
转场进行进一步的定制。实际上该类也可以在.FullScreen模式下使用,但是会丢失由该类负责的动画,保险起见还是遵循官方的建议,只在.Custom模式下使用该类。官方对该类参与转场的流程和使用方法有非常详细的说明:Creating
Custom
Presentations。

UIPresentationController类主要给 Modal 转场带来了以下几点变化:

定制 presentedView 的外观:设定 presentedView 的尺寸以及在 containerView
中添加自定义视图并为这些视图添加动画;

可以选择是否移除 presentingView;

可以在不需要动画控制器的情况下单独工作;

iOS 8 中的适应性布局。

以上变化中第1点 iOS 7 中也能做到,3和4是 iOS 8
带来的新特性,只有第2点才真正解决了 iOS 7 中的痛点。在 iOS 7
中定制外观时,动画控制器需要负责管理额外添加的的视图,UIPresentationController类将该功能剥离了出来独立负责,其提供了如下的方法参与转场,对转场过程实现了更加细致的控制,从命名便可以看出与动画控制器里的animateTransition:的关系:

func presentationTransitionWillBegin()

func presentationTransitionDidEnd(_ completed: Bool)

func dismissalTransitionWillBegin()

func dismissalTransitionDidEnd(_ completed: Bool)

除了
presentingView,UIPresentationController类拥有转场过程中剩下的角色:

//指定初始化方法。

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?

没有 presentingView 是因为 Custom 模式下 presentingView 不受
containerView 管理,UIPresentationController类并没有改变这一点。iOS 8
扩充了转场环境协议,可以通过viewForKey:方便获取转场的视图,而该方法在
Modal 转场中获取的是presentedView()返回的视图。因此我们可以在子类中将
presentedView 包装在其他视图后重写该方法返回包装后的视图当做
presentedView 在动画控制器中使用。

接下来,我用UIPresentationController子类实现上一节「Modal
转场实践」里的效果,presentingView 和 presentedView
的动画由动画控制器负责,剩下的事情可以交给我们实现的子类来完成。

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

转场协调器遵守协议,它含有以下几个方法:

//与动画控制器中的转场动画同步,执行其他动画

animateAlongsideTransition:completion:

//与动画控制器中的转场动画同步,在指定的视图内执行动画

animateAlongsideTransitionInView:animation:completion:

由于转场协调器的这种特性,动画的同步问题解决了。

class OverlayPresentationController: UIPresentationController {

let dimmingView = UIView()

//Presentation 转场开始前该方法被调用。

override func presentationTransitionWillBegin() {

self.containerView?.addSubview(dimmingView)

let initialWidth = containerView!.frame.width*2/3, initialHeight =
containerView!.frame.height*2/3

self.dimmingView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)

self.dimmingView.center = containerView!.center

self.dimmingView.bounds = CGRect(x: 0, y: 0, width: initialWidth ,
height: initialHeight)

//使用 transitionCoordinator 与转场动画并行执行 dimmingView 的动画。

presentedViewController.transitionCoordinator()?.animateAlongsideTransition({
_ in

self.dimmingView.bounds = self.containerView!.bounds

}, completion: nil)

}

//Dismissal 转场开始前该方法被调用。添加了 dimmingView
消失的动画,在上一节中并没有添加这个动画,

//实际上由于 presentedView
的形变动画,这个动画根本不会被注意到,此处只为示范。

override func dismissalTransitionWillBegin() {

presentedViewController.transitionCoordinator()?.animateAlongsideTransition({
_ in

self.dimmingView.alpha = 0.0

}, completion: nil)

}

}

OverlayPresentationController类接手了 dimmingView
的工作后,需要回到上一节OverlayAnimationController里把涉及 dimmingView
的部分删除,然后在 presentedVC
的转场代理属性transitioningDelegate中提供该类实例就可以实现和上一节同样的效果。

func presentationControllerForPresentedViewController(_ presented:
UIViewController,

presentingViewController presenting: UIViewController,

sourceViewController source: UIViewController) ->
UIPresentationController?{

return OverlayPresentationController(presentedViewController: presented,
presentingViewController: presenting)

}

在 iOS 7 中,Custom 模式的 Modal 转场里,presentingView
不会被移除,如果我们要移除它并妥善恢复会破坏动画控制器的独立性使得第三方动画控制器无法直接使用;在
iOS 8
中,UIPresentationController解决了这点,给予了我们选择的权力,通过重写下面的方法来决定
presentingView 是否在 presentation 转场结束后被移除:

func shouldRemovePresentersView() -> Bool

返回 true 时,presentation 结束后 presentingView 被移除,在 dimissal
结束后 UIKit 会自动将 presentingView
恢复到原来的视图结构中。此时,Custom 模式与 FullScreen
模式下无异,完全不必理会前面 dismissal
转场部分的差异了。另外,这个方法会在实现交互控制的 Modal
转场时起到关键作用,详情请看交互转场部分。

你可能会疑惑,除了解决了 iOS 7 中无法干涉 presentingView
这个痛点外,还有什么理由值得我们使用UIPresentationController类?除了能与动画控制器配合,UIPresentationController类也能脱离动画控制器独立工作,在转场代理里我们仅仅提供后者也能对
presentedView 的外观进行定制,缺点是无法控制 presentedView
的转场动画,因为这是动画控制器的职责,这种情况下,presentedView
的转场动画采用的是默认的 Slide Up
动画效果,转场协调器实现的动画则是采用默认的动画时间。

iOS 8
带来了适应性布局,协议用于响应视图尺寸变化和屏幕旋转事件,之前用于处理屏幕旋转的方法都被废弃了。UIViewController
和 UIPresentationController 类都遵守该协议,在 Modal
转场中如果提供了后者,则由后者负责前者的尺寸变化和屏幕旋转,最终的布局机会也在后者里。在OverlayPresentationController中重写以下方法来调整视图布局以及应对屏幕旋转:

override func containerViewWillLayoutSubviews() {

self.dimmingView.center = self.containerView!.center

self.dimmingView.bounds = self.containerView!.bounds

let width = self.containerView!.frame.width * 2 / 3, height =
self.containerView!.frame.height * 2 / 3

self.presentedView()?.center = self.containerView!.center

self.presentedView()?.bounds = CGRect(x: 0, y: 0, width: width, height:
height)

}

转场代理

完成动画控制器后,只需要在转场前设置好转场代理便能实现动画控制器中提供的效果。转场代理的实现也很简单,但是在设置代理时有不少陷阱,需要注意。

UINavigationControllerDelegate

定制 UINavigationController
这种容器控制器的转场时,很适合实现一个子类,自身集转场代理,动画控制器于一身,也方便使用,不过这样做有时候又限制了它的使用范围,别人也实现了自己的子类时便不能方便使用你的效果,下面的范例采取的是将转场代理封装成一个类。

class SDENavigationControllerDelegate: NSObject,
UINavigationControllerDelegate {

//在对象里,实现该方法提供动画控制器,返回 nil 则使用系统默认的效果。

func navigationController(navigationController: UINavigationController,

animationControllerForOperation operation:
UINavigationControllerOperation,

fromViewController fromVC: UIViewController,

toViewController toVC: UIViewController) ->
UIViewControllerAnimatedTransitioning? {

//使用上一节实现的 Slide 动画控制器,需要提供操作类型信息。

let transitionType = SDETransitionType.NavigationTransition(operation)

return SlideAnimationController(type: transitionType)

}

}

如果你在代码里为你的控制器里这样设置代理:

//错误的做法,delegate
是弱引用,在离开这行代码所处的方法范围后,delegate 将重新变为
nil,然后什么都不会发生。

self.navigationController?.delegate = SDENavigationControllerDelegate()

可以使用强引用的变量来引用新实例,且不能使用本地变量,在控制器中新增一个变量来维持新实例就可以了。

self.navigationController?.delegate = strongReferenceDelegate

解决了弱引用的问题,这行代码应该放在哪里执行呢?很多人喜欢在viewDidLoad()做一些配置工作,但在这里设置无法保证是有效的,因为这时候控制器可能尚未进入
NavigationController 的控制器栈,self.navigationController返回的可能是
nil;如果是通过代码 push 其他控制器,在 push
前设置即可;prepareForSegue:sender:方法是转场前更改设置的最后一次机会,可以在这里设置;保险点,使用UINavigationController子类,自己作为代理,省去到处设置的麻烦。

不过,通过代码设置终究显得很繁琐且不安全,在 storyboard
里设置一劳永逸:在控件库里拖拽一个 NSObject 对象到相关的
UINavigationControler
上,在控制面板里将其类别设置为SDENavigationControllerDelegate,然后拖拽鼠标将其设置为代理。

最后一步,像往常一样触发转场:

self.navigationController?.pushViewController(toVC, animated: true)//or

self.navigationController?.popViewControllerAnimated(true)

Demo
地址:NavigationControllerTransition。

UITabBarControllerDelegate

同样作为容器控制器,UITabBarController 的转场代理和
UINavigationController
类似,通过类似的方法提供动画控制器,不过的代理方法里提供了操作类型,但的代理方法没有提供滑动的方向信息,需要我们来获取滑动的方向。

class SDETabBarControllerDelegate: NSObject, UITabBarControllerDelegate
{

//在对象里,实现该方法提供动画控制器,返回 nil 则没有动画效果。

func tabBarController(tabBarController: UITabBarController,
animationControllerForTransitionFromViewController

fromVC: UIViewController,

toViewController toVC: UIViewController) ->
UIViewControllerAnimatedTransitioning?{

let fromIndex = tabBarController.viewControllers!.indexOf(fromVC)!

let toIndex = tabBarController.viewControllers!.indexOf(toVC)!

let tabChangeDirection: TabOperationDirection = toIndex < fromIndex ?
.Left : .Right

let transitionType = SDETransitionType.TabTransition(tabChangeDirection)

let slideAnimationController = SlideAnimationController(type:
transitionType)

return slideAnimationController

}

}

为 UITabBarController 设置代理的方法和陷阱与上面的
UINavigationController 类似,注意delegate属性的弱引用问题。点击 TabBar
的相邻页面进行切换时,将会看到 Slide
动画;通过以下代码触发转场时也将看到同样的效果:

tabBarVC.selectedIndex = …//or

tabBarVC.selectedViewController = …

Demo
地址:ScrollTabBarController。

UIViewControllerTransitioningDelegate

Modal 转场的代理协议是 iOS 7 新增的,其为 presentation 和 dismissal
转场分别提供了动画控制器。前面实现的OverlayAnimationController类可同时处理
presentation 和 dismissal 转场。UIPresentationController只在 iOS
8中可用,通过available关键字可以解决 API 的版本差异。

class SDEModalTransitionDelegate: NSObject,
UIViewControllerTransitioningDelegate {

func animationControllerForPresentedController(presented:
UIViewController,

presentingController presenting: UIViewController,

sourceController source: UIViewController) ->
UIViewControllerAnimatedTransitioning? {

return OverlayAnimationController()

}

func animationControllerForDismissedController(dismissed:
UIViewController) -> UIViewControllerAnimatedTransitioning? {

return OverlayAnimationController()

}

@available(iOS 8.0, *)

func presentationControllerForPresentedViewController(presented:
UIViewController,

presentingViewController presenting: UIViewController,

sourceViewController source: UIViewController) ->
UIPresentationController? {

return OverlayPresentationController(presentedViewController: presented,
presentingViewController: presenting)

}

}

Modal 转场的代理由 presentedVC
的transitioningDelegate属性来提供,这与前两种容器控制器的转场不一样,不过该属性作为代理同样是弱引用,记得和前面一样需要有强引用的变量来维护该代理,而
Modal 转场需要 presentedVC 来提供转场代理的特性使得 presentedVC
自身非常适合作为自己的转场代理。另外,需要将 presentedVC
的modalPresentationStyle属性设置为.Custom或.FullScreen,只有这两种模式下才支持自定义转场,该属性默认值为.FullScreen。自定义转场时,决定转场动画效果的modalTransitionStyle属性将被忽略。

开启转场动画的方式依然是两种:在 storyboard 里设置 segue
并开启动画,但这里并不支持.Custom模式,不过还有机会挽救,转场前的最后一个环节prepareForSegue:sender:方法里可以动态修改modalPresentationStyle属性;或者全部在代码里设置,示例如下:

let presentedVC = …

presentedVC.transitioningDelegate =
strongReferenceSDEModalTransitionDelegate

//当与 UIPresentationController 配合时该属性必须为.Custom。

presentedVC.modalPresentationStyle = .Custom/.FullScreen

presentingVC.presentViewController(presentedVC, animated: true,
completion: nil)

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图