iOS视图控制器编程指南-Part 3:弹出和过渡

翻译自“View Controller Programming Guide for iOS”。

1 弹出视图控制器

在屏幕上显示视图控制器有两种方式:嵌入容器视图控制器或者弹出视图控制器。容器视图控制器提供了应用程序的主要导航,但弹出视图控制器也是一个重要的导航工具。可以在当前视图控制器上直接显示一个新的视图控制器。实现模态界面时,通常使用弹出视图控制器,但也可以用于其它目的。

UIViewController类内置支持弹出视图控制器,可用于所有视图控制器对象。可以从任何视图控制器弹出视图控制器,但UIKit可能重路由该请求到另一个视图控制器。弹出一个视图控制器会在原始试图控制器之间建立关系,原始视图控制器称为presenting视图控制器,显示的新视图控制器称为presented视图控制器。这种关系是视图控制器层级结构的一部分,会一直存在直到presented视图控制器消失。

1.1 弹出和过渡过程

弹出视图控制器是一种快速和简单的方式,可以在屏幕上动画的显示新内容。UIKit内置的弹出机制可以使用内置或自定义动画显示新的视图控制器。因为UIKit处理了所有工作,所以内置弹出和动画只需要编写很少代码。也可以做些额外工作创建自定义弹出和动画,然后在任何视图控制器中使用它们。

可以通过代码或segue发起视图控制器的弹出。如果设计时知道应用程序的导航方式,segue是最简单的方式。对于动态界面,或者没有可用控件发起segue时,使用UIViewController的方法弹出视图控制器。

1.1.1 弹出风格

视图控制器的弹出风格决定了它在屏幕上的外观。UIKit定义了很多标准弹出风格,每一种都有特定的外观和意图。也可以定义自己的弹出风格。设计应用程序时,选择最合理的弹出风格,并为想要弹出的视图控制器的modalPresentationStyle属性指定适当的常量。

1.1.1.1 全屏(Full-Screen)弹出风格

全屏弹出风格覆盖整个屏幕,并阻止与下面内容的交互。在水平方向为常规的环境中,只有其中一种全屏风格完全覆盖下面的内容。其余的风格包含模糊的视图或透明度,允许下面的视图控制器部分显示。在水平方向为紧凑的环境中,全屏弹出自动适应为UIModalPresentationFullScreen风格,并覆盖下面所有内容。

图8-1展示了水平方向为常规的环境中,UIModalPresentationFullScreen,UIModalPresentationPageSheet和UIModalPresentationFormSheet风格的弹出外观。图中,左上角的绿色视图控制器弹出右上角的蓝色视图控制器,每种弹出风格的结果在下面显示。对于某些弹出风格,UIKit在两个视图控制器的内容之间插入一个模糊视图。

图8-1 全屏弹出风格

提示:使用UIModalPresentationFullScreen风格弹出视图控制器时,通常UIKit会在过渡动画完成后移除下面的视图控制器的视图。可以通过指定UIModalPresentationOverFullScreen风格来阻止移除这些视图。当presented视图控制器有一个透明区域可以让下面的内容显示时,可以使用这种风格。

使用其中一种全屏弹出风格时,发起该弹出的视图控制器本身必须覆盖整个屏幕。如果presenting视图控制器没有覆盖整个屏幕,UIKit遍历视图控制器层级结构,直到找到一个覆盖整个屏幕的视图控制器。如果不能找到充满屏幕的视图控制器,UIKit使用窗口的根视图控制器。

1.1.1.2 弹出框(Popover)风格

UIModalPresentationPopover风格在一个弹出框视图上显示视图控制器。弹出框用于显示额外的信息,或者当前焦点和选中对象相关的项目列表。在水平方向为常规的环境中,弹出框视图覆盖屏幕的一部分,如图8-2所示。在水平方向为紧凑的环境中,弹出框默认适应为UIModalPresentationOverFullScreen弹出风格。在弹出框视图外点击会让它自动消失。

图8-2 弹出框风格

在水平方向为紧凑的环境中,弹出框会采用全屏弹出,所以通常需要修改弹出框代码。在全屏模式中,需要一种方式让弹出框消失。此时可以添加按钮,嵌入弹出框到一个可消失的容器视图控制器中,或者改变自适应行为本身。

如何配置弹出框,请参考“Presenting a View Controller in a Popover”。

1.1.1.3 当前上下文(Current Context)风格

UIModalPresentationCurrentContext风格覆盖界面的一个特定视图控制器。使用上下文风格时,通过设置视图控制器的definesPresentationContext属性为YES来指定覆盖该视图控制器。图8-3中,当前上下文弹出只覆盖了分割视图控制器的一个子视图控制器。

图8-3 当前上下文弹出风格

提示:使用UIModalPresentationFullScreen风格弹出视图控制器时,通常UIKit会在过渡动画完成后移除下面的视图控制器的视图。可以通过指定UIModalPresentationOverCurrentContext风格来阻止移除这些视图。当presented视图控制器有一个透明区域可以让下面的内容显示时,可以使用这种风格。

定义弹出上下文的视图控制器还可以定义弹出过程中的过渡动画。通常,UIKit使用presented视图控制器的modalTransitionStyle属性值让视图控制器产生动画。如果弹出上下文视图控制器设置自己的providesPresentationContextTransitionStyle属性为YES,UIKit使用该视图控制器的modalTransitionStyle属性值代替。

过渡到水平方向为紧凑环境时,当前上下文风格适应为UIModalPresentationFullScreen风格。通过使用自适应弹出代理指定不同的弹出风格或视图控制器来改变这种行为。

1.1.1.4 自定义弹出风格

UIModalPresentationCustom风格可以使用自定义风格弹出视图控制器。创建自定义风格涉及继承UIPresentationController,使用它的方法动画的移动自定义视图到屏幕上,并设置presented视图控制器的尺寸和位置。弹出控制器还需要处理presented视图控制器的特征变化时引起的所有适配工作。

如何定义自定义弹出视图控制器的信息,请参考“创建自定义弹出”。

1.1.2 过渡(Transition)风格

过渡风格决定了显示presented视图控制器的动画类型。对于内置的过渡风格,可以为想要弹出的视图控制器的modalTransitionStyle属性指定一个标准过渡风格。弹出视图控制器时,UIKit创建相应的动画风格。例如,图8-4展示了标准的上滑式(slide-up)过渡(UIModalTransitionStyleCoverVertical)如何动画的移动视图口控制器到屏幕上。视图控制器B从屏幕外开始,动画向上覆盖顶层的视图控制器A。视图控制器B消失时,动画反转,B下滑显示A。

图8-4 视图控制器的过渡动画

使用动画对象和过渡代理创建自定义过渡。动画对象为在屏幕上放置视图控制器创建过渡动画。过渡代理在适当的时候为UIKit提供动画对象。如何实现自定义过渡,请参考“自定义过渡动画”。

1.1.3 弹出VS显示视图控制器

UIViewController类提供两种方式显示视图控制器:

  • showViewController:sender:和showDetailViewController:sender:方法是显示视图控制器最合适和灵活的方式。这些方法让presenting视图控制器决定最好的弹出方式。例如,容器视图控制器可能把视图控制器作为子视图控制器,而不是模态的弹出。默认以模态方式弹出视图控制器。
  • presentViewController:animated:completion:方法总是模态显示视图控制器。调用该方法的视图控制器可能根本不处理弹出,但总是以模态弹出。在水平方向为紧凑的环境中,该方法适用于弹出风格。

showViewController:sender:和showDetailViewController:sender:方法是发起弹出的首选方式。视图控制器可以在完全不知道视图控制器层级结构中其它视图控制器,或者当前视图控制器在该层级结构中位置的情况下调用这些方法。这些方法可以更容易重用视图控制器,而不需要编码额外的条件代码路径。

1.2 弹出视图控制器

有几种方式可以弹出视图控制器:

  • 使用segue自动弹出视图控制器。Segue使用界面生成器中指定的信息实例化并弹出视图控制器。更多如何配置segue的信息,请参考“使用Segues”。
  • 调用showViewController:sender:或showDetailViewController:sender:方法显示视图控制器。在自定义视图控制器中,可以改变这些方法的行为,让她们更适合你的视图控制器。
  • 调用presentViewController:animated:completion:方法模态的弹出视图控制器。

关于如何关闭视图控制器,请参考“关闭Presented视图控制器”。

1.2.1 显示视图控制器

使用showViewController:sender:或showDetailViewController:sender:方法时,在屏幕上显示一个新视图控制器的过程很简单:

  1. 创建要弹出的视图控制器对象。创建时,用它需要的数据初始化它。
  2. 设置新视图控制器的modalPresentationStyle属性为首选的弹出风格。该风格可能不是最终弹出的风格。
  3. 设置视图控制器的modalTransitionStyle属性为期望的过渡动画风格。该风格可能不是最终的动画风格。
  4. 调用当前视图控制器的showViewController:sender:和showDetailViewController:sender:方法。

UIKit转发showViewController:sender:和showDetailViewController:sender:方法的调用给合适的presenting视图控制器。该视图控制器决定如何最好的执行弹出,并根据需要修改弹出和过渡风格。例如,导航控制器可能把视图控制器压入它的导航栈。

关于显示视图控制器和以模态的方式弹出视图控制器之间的差异,请参考“弹出VS显示视图控制器”。

1.2.2 模态的弹出视图控制器

直接弹出一个视图控制器时,需要告诉UIKit你想如何显示新视图控制器,以及如何动画的出现在屏幕上。

  1. 创建想要弹出的视图控制器。
  2. 设置新视图控制器的modalPresentationStyle属性为首选的弹出风格。
  3. 设置视图控制器的modalTransitionStyle属性为期望的过渡动画风格。
  4. 调用当前视图控制器的presentViewController:animated:completion:方法。

调用presentViewController:animated:completion:方法的视图控制器可能不是实际执行模态弹出的那个视图控制器。弹出风格决定视图控制器如何被弹出,包括presenting视图控制器需要的特征。例如,全屏弹出要求由全屏视图控制器发起。如果当前的presenting视图控制器不合适,UIKit遍历视图控制器层级结构,直到找到一个合适的。完成模态弹出后,UIKit更新受影响的视图控制器的presentingViewController和presentedViewController属性。

列表8-1描述了如何通过代码弹出视图控制器。用户添加新食谱时,应用程序弹出一个导航控制器提示用户输入食谱的基本信息。导航控制器有标准地方放置取消和完成按钮。使用导航控制器也方便将来扩展新是食谱界面。所要做的所有工作就是把新视图控制器压入导航栈。

列表8-1 通过代码弹出视图控制器

- (void)add:(id)sender {
   // Create the root view controller for the navigation controller
   // The new view controller configures a Cancel and Done button for the
   // navigation bar.
   RecipeAddViewController *addController = [[RecipeAddViewController alloc] init];
 
   addController.modalPresentationStyle = UIModalPresentationFullScreen;
   addController.transitionStyle = UIModalTransitionStyleCoverVertical;
   [self presentViewController:addController animated:YES completion: nil];
}

1.2.3 在弹出框中弹出视图控制器

弹出框显示之前需要额外的配置。设置模态弹出类型为UIModalPresentationPopover后,配置以下弹出相关的属性:

  • 设置视图控制器的preferredContentSize属性为期望的尺寸。
  • 使用关联的UIPopoverPresentationController对象设置弹出锚点,该对象通过视图控制器的popoverPresentationController属性访问。设置以下的其中一个:
  • 设置barButtonItem属性为一个导航按钮项。
  • 设置sourceView和sourceRect为其中一个视图的指定区域。

根据需要,可以使用UIPopoverPresentationController对象调整更多弹出框的外观。Popover presentation控制器也支持代理对象,可以用来响应弹出过程中的变化。例如,使用代理响应弹出框出现,消失或者重定位。

1.3 关闭Presented视图控制器

调用presenting视图控制器的dismissViewControllerAnimated:completion:方法让presented视图控制器消失。也可以在presented视图控制器本身调用该方法。如果在presented视图控制器中调用该方法,UIKit自动转发该请求给presenting视图控制器。

关闭视图控制器之前,保存该视图控制器的所有重要信息。关闭视图控制器会把它从视图控制器层级结构移除,并从屏幕上移除它的视图。如果没有存储指向该视图控制器的强引用,关闭会释放关联的内容。

使用代理设计模式从presented视图控制器返回数据给presenting视图控制器。代理可以更容易的重用视图控制器。使用代理时,presented视图控制器存储一个指向代理对象的引用,该对象实现正式协议的方法。Presented视图控制器调用代理的这些方法收集结果。典型的做法是,presenting视图控制器本身作为presented视图控制器的代理。

1.4 显示另一个故事版中定义的视图控制器

可以在同一个故事版的视图控制器之间创建segue,但不能在不同故事版之间创建segue。想要显示另一个故事版中的视图控制器,必须在弹出前显式的实例化该视图控制器,如列表8-2所示。该例子模态弹出视图控制器,你也可以把它压入导航控制器,或者其它方式显示。

列表8-2 从故事版中加载视图控制器

UIStoryboard* sb = [UIStoryboard storyboardWithName:@"SecondStoryboard" bundle:nil];
MyViewController* myVC = [sb instantiateViewControllerWithIdentifier:@"MyViewController"];
 
// Configure the view controller.
 
// Display the view controller
[self presentViewController:myVC animated:YES completion:nil];

没有要求必须创建多个故事版。不过有些情况下,多个故事版很有用:

  • 有一个很大的开发团队,不同的用户界面由不同人员开发。每个团队在自己的故事版中开发界面可以最小化冲突。
  • 购买或创建的库中预定义了视图控制器类型集合;这些视图控制器的内容由库中的故事版中提供。
  • 有内容需要在外接屏幕上显示。这种情况下,可以保持所有与另一个屏幕相关的视图控制器在单独的故事版中。这种场景下的另一种方式是编写自定义segue。

2 使用Segues

使用segue定义应用程序界面流。一个segue定义了故事版中两个视图控制器之间的过渡。Segue的起点可以是发起segue的按钮,表格行或者手势识别器。终点是想要显示的视图控制器。Segue总是弹出新视图控制器,也可以使用unwind segue关闭一个视图控制器。

图9-1 两个视图控制器之间的segue

不需要通过代码触发segue。运行时,UIKit记载关联视图控制器的segue,并连接它们到相应的元素。用户与元素交互时,UIKit加载适当的视图控制器,通知应用程序segue即将发生,并执行过渡。可以使用UIKit发送的通知,将数据传递给新视图控制器,或者干脆阻止segue的发生。

2.1 在视图控制器之间创建Segue

通过Control-click第一个视图控制器中的适当元素,并拖拽到目标视图控制器,可以在同一个故事版文件的视图控制器之间创建segue。Segue的起点必须是视图或者定义了action的对象,例如控件,导航栏按钮项(bar button item)或者手势识别器。也可以从基于单元格的视图,例如表格和集合视图,创建segue。图9-2显示了如何创建segue,点击表格行时,显示新视图控制器。

图9-2 创建segue关系

提示:有些元素支持多个segues。例如,表格行可以为点击行的辅助按钮和其它部分配置不同的segue。

释放鼠标按钮后,界面生成器提示你选择两个视图控制器之间的关系类型,如图9-3所示。选择与过渡对应的segue。

图9-3 选择创建的segue类型

选择segue的关系类型时,尽可能选择自适应segue。自适应segue根据当前环境自动调整它们的行为。比如,一个Show segue根据presenting视图控制器改变行为。提供非自适应segue的应用程序必须也能在iOS 7上运行,因为它不支持自适应segue。表格9-1列出了自适应segues,以及它们在应用程序中的行为。

表格9-1 自适应segue类型

Segue类型 行为
Show (Push) 该segue使用目标视图控制器的showViewController:sender:方法显示新内容。对于大部分视图控制器,该segue在源视图控制器上模态显示新内容。有些视图控制器覆写该方法实现不同的行为。例如,导航控制器把新视图控制器压入导航栈。
UIkit使用targetViewControllerForAction:sender:方法定位源视图控制器。
Show Detail (Replace) 该segue使用目标视图控制器的showDetailViewController:sender:方法显示新内容。该segue只与嵌入UISplitViewController对象的视图控制器相关。使用该segue,分割视图控制器用新内容代替第二个子视图控制器(详细视图控制器)。其它大部分视图控制器模态的显示新内容。
UIKit使用targetViewControllerForAction:sender:方法定位源视图控制器。
Present Modally 该segue使用指定的弹出和过渡风格显示视图控制器。定义了适当弹出上下文的视图控制器处理实际的弹出。
Present as Popover 在水平方向为常规的环境中,视图控制器以弹出框的形式出现。在水平方向为紧凑的环境中,视图控制器使用全屏模态方式弹出视图控制器。

创建segue后,选中segue对象,使用属性(attributes)检查器指定一个标识符。在segue期间,使用该标识符确定触发了哪个segue,尤其在视图控制器支持多个segues时很有用。执行segue时,包含该标识符的UIStoryboardSegue对象会传递给视图控制器。

2.2 运行时修改Segue的行为

图9-4显示了触发segue时发生了什么。大部分工作发生在presenting视图控制器中,它管理过渡到新视图控制器。新视图控制器的配置过程基本与自己创建视图控制器并弹出它相同。因为segue在故事版中配置,所以segue涉及的两个视图控制器必须在同一个故事版中。

图9-4 使用segue显示视图控制器

在segue期间,UIKit调用当前视图控制器的方法,让你有机会控制segue的结果。

  • 在shouldPerformSegueWithIdentifier:sender:方法可以阻止segue发生。该方法返回NO会导致segue悄悄地失败,但不会阻止其它动作发生。例如,点击表格行仍然会导致表格调用相关的代理方法。
  • 源视图控制器的prepareForSegue:sender:可以传递数据到目标视图控制器。传递到该方法的UIStoryboardSegue对象包含一个目标视图控制器的引用和其它segue相关的信息。

2.3 创建Unwind Segue

Unwind segue可以关闭弹出的视图控制器。通过在界面生成器中链接按钮或其它合适对象到当前视图控制器的Exit对象来创建unwind segue。用户点击按钮或与其它合适对象交互时,UIKit搜索在视图控制器层级结构中搜索可以处理unwind segue的对象。然后关闭当前视图控制器和unwind segue目标之间的所有视图控制器。

创建unwind segue的步骤如下:

  1. 选择unwind segue结束后显示在屏幕上的视图控制器。
  2. 在选择的视图控制器中定义unwind action方法。
    该方法的Swift语法如下:
@IBAction func myUnwindAction(unwindSegue: UIStoryboardSegue)

该方法的Objective-C语法如下:

- (IBAction)myUnwindAction:(UIStoryboardSegue*)unwindSegue
  1. 导航到发起unwind action的视图控制器。
  2. Control-click发起unwind segue的按钮(或其它对象)。该元素在想要关闭的视图控制器中。
  3. 拖拽到视图控制器场景顶部的Exit对象。
  4. 从关系面板上选择unwind action方法。

在界面生成器中创建相应的unwind segue之前,必须在其中一个视图控制器中定义unwind action方法。该方法是必须的,它告诉界面生成器unwind segue有一个有效的目标。

在unwind action方法中执行应用程序的具体任务。不用自己关闭涉及segue的任何视图控制器;UIKit替你完成了这项工作。相反,使用segue对象获得被关闭的视图控制器,并从中接收数据。也可以在unwind segue完成之前,在unwind action中更新当前视图控制器。

2.4 通过代码发起Segue

通常,segue的触发是因为在故事版文件中创建的连接。然而,有些时候不能在故事版中创建segue,可能因为目标视图控制器还没有确定。例如,一个游戏应用程序可能根据游戏的结果过渡到不同界面。这些情况下,可以在当前视图控制器中调用performSegueWithIdentifier:sender:方法,通过代码触发segue。

列表9-1中,当从竖屏旋转到横屏时,segue弹出一个特殊的视图控制器。因为通知对象没有为执行segue命令提供有用的信息,所以视图控制器指定自己为segue的sender。

列表9-1 通过代码触发segue

- (void)orientationChanged:(NSNotification *)notification {
    UIDeviceOrientation deviceOrientation = [UIDevice currentDevice].orientation;
    if (UIDeviceOrientationIsLandscape(deviceOrientation) &&
             !isShowingLandscapeView) {
        [self performSegueWithIdentifier:@"DisplayAlternateView" sender:self];
        isShowingLandscapeView = YES;
    }
// Remainder of example omitted.
}

2.5 创建自定义Segue

界面生成器为所有从一个视图控制器过渡到另一个(从presenting视图控制器到在弹出框中显示视图控制器)的标准方式提供segue。如果这些都不是你想要的,可以创建自定义segue。

2.5.1 Segue的生命周期

要理解自定义segue如何工作,需要理解segue对象的生命周期。Segue对象是UIStoryboardSegue类或它的子类的实例。应用程序永远不是直接创建segue对象;Segue触发时,UIKit自动创建它们。以下是创建segue的过程:

  1. 创建和初始化被弹出的视图控制器。
  2. 创建segue对象,并调用它的initWithIdentifier:source:destination:方法。Segue的标识符是在界面生成器中提供的唯一字符串,另外两个参数代表过渡中的两个控制器对象。
  3. 调用presenting视图控制器的prepareForSegue:sender:方法。参考“运行时修改Segue的行为”。
  4. 调用segue对象的perform方法。该方法执行过渡,把新视图控制器显示在屏幕上。
  5. 释放segue对象的引用。

2.5.2 实现自定义Segue

通过继承UIStoryboardSegue并实现以下方法来实现自定义segue:

  • 覆写initWithIdentifier:source:destination:方法,并用它初始化自定义segue对象。总要先调用super方法。
  • 实现perform方法,并用它配置过渡动画。

提示:如果实现中添加了属性来配置segue,不能再界面生成器中配置这些属性。相反,在触发segue的源视图控制器的prepareForSegue:sender:方法中配置自定义的segue的额外属性。

列表9-2是一个很简单的自定义segue。这个例子不用任何形式的动画弹出目标视图控制器,但可以扩展实现自己的动画。

列表9-2 自定义segue

- (void)perform {
    // Add your own animation code here.
 
    [[self sourceViewController] presentViewController:[self destinationViewController] animated:NO completion:nil];
}

3 自定义过渡动画

过渡动画为应用程序界面变化提供视觉反馈。UIKit提供了一组标准过渡类型用于弹出视图控制器,可以通过自定义过渡补充标准过渡。

3.1 过渡动画序列

过渡动画交换两个视图控制器的内容。有两种类型的过渡:弹出和关闭(dismissal)。弹出过渡添加一个新视图控制器到视图控制器层级结构,而关闭过渡从层级结构中移除一个或多个视图控制器。

实现过渡动画涉及很多对象。UIKit提供了所有涉及过渡的对象的默认版本,可以自定义所有或其中一部分对象。如果选择了一组正确的对象,只需要少量代码就能创建动画。如果利用UIKit提供的现有代码,甚至可以很容易创建包括交互的动画。

3.1.1 过渡代理

过渡代理是过渡动画和自定义弹出的起点。过渡代理是遵循UIViewControllerTransitioningDelegate协议的对象。它的任务是为UIKit提供以下对象:

  • 动画对象。动画对象负责创建动画,用于显示或隐藏视图控制器的视图。过渡代理可以为显示和关闭视图控制器提供独立的动画对象。动画对象遵循UIViewControllerAnimatedTransitioning协议。
  • 交互式动画对象。交互式动画对象使用触摸事件或手势识别器驱动自定义动画的时间。交互式动画对象遵循UIViewControllerInteractiveTransitioning协议。
    创建交互式动画对象最简单的方式是继承UIPercentDrivenInteractiveTransition类,并在子类中添加事件处理代码。该类使用现有的动画对象控制动画创建时间。如果创建自己的可交互动画对象,必须自己渲染动画的每一帧。
  • 弹出(presentation)控制器。弹出控制器管理视图控制器出现在屏幕上的弹出风格。系统为内置的弹出风格提供了弹出控制器,也可以为自己的弹出风格提供自定义弹出控制器。更多关于创建自定义弹出控制器的信息,请参考“创建自定义弹出”。

给视图控制器的transitioningDelegate属性指定一个过渡代理,告诉UIKit你想执行一个自定义过渡或弹出。可以选择代理提供哪些对象。如果不提供动画对象,UIKit使用视图控制器的modalTransitionStyle属性中的标准过渡动画。

图10-1显示了presented试图控制器中过渡代理和动画对象之间的关系。只有当视图控制器的modalPresentationStyle属性设置为UIModalPresentationCustom时,才使用弹出控制器。

图10-1 自定义弹出和动画对象

关于如何实现过渡代理,请参考“实现过渡代理”。关于过渡代理的方法,请参考“UIViewControllerTransitioningDelegate Protocol Reference“。

3.1.2 自定义动画序列

当presented视图控制器的transitioningDelegate包含一个有效的对象时,UIKit使用你提供的自定义动画对象弹出视图控制器。准备弹出时,UIKit调用过渡代理的animationControllerForPresentedController:presentingController:sourceController:方法接收自定义动画对象。如果对象可用,UIKit执行以下步骤:

  1. UIKit调用过渡代理的interactionControllerForPresentation:方法,查看交互式动画对象是否可用。如果该方法返回nil,UIKit执行没有用户交互的动画。
  2. UIKit调用动画对象的transitionDuration:方法获得动画持续时间。
  3. UIKit调用合适的方法启动动画:
  • 对于非交互式动画,UIKit调用动画对象的animateTransition:方法。
  • 对于交互式动画,UIKit调用交互式动画对象的startInteractiveTransition:方法。
  1. UIKit等待动画对象调用上下文过渡对象的completeTransition:方法。

    动画完成后,自定义动画对象调用该方法,典型的做法是在动画完成块中。调用该方法结束过渡,让UIKit知道,它可以调用完成处理器的presentViewController:animated:completion:方法和动画对象本身的animationEnded:方法。

关闭(dismiss)视图控制器时,UIKit调用过渡代理的animationControllerForDismissedController:方法,并执行以下步骤:

  1. UIKit调用过渡代理的interactionControllerForDismissal:方法,查看可交互动画对象是否可用。如果该方法返回nil,UIKit执行没有用户交互的动画。
  2. UIKit调用动画对象的transitionDuration:方法获得动画持续时间。
  3. UIKit调用合适的方法启动动画:
  • 对于非交互式动画,UIKit调用动画对象的animateTransition:方法。
  • 对于交互式动画,UIKit调用交互式动画对象的startInteractiveTransition:方法。
  1. UIKit等待动画对象调用上下文过渡对象的completeTransition:方法。

    动画完成后,自定义动画对象调用该方法,典型的做法是在动画完成块中。调用该方法结束过渡,让UIKit知道,它可以调用完成处理器的presentViewController:animated:completion:方法和动画对象本身的animationEnded:方法。

重要:必须在动画结束时调用completeTransition:方法。UIKit不会结束过渡过程,所以直到调用该方法,才会返回把控制返回应用程序。

3.1.3 过渡上下文对象

过渡动画开始之前,UIKit创建过渡上下文对象,并用如何执行动画的信息填充。过渡上下文对象是代码中的重要部分。它实现了UIViewControllerContextTransitioning协议,并储存过渡中涉及的视图控制器和视图的引用。还存储应该如何执行过渡的信息,包括动画是否可交互。动画对象需要这些所有信息来设置和执行实际的动画。

重要:设置自定义动画时,总是使用过渡上下文对象中的对象和属性,而不是自己管理的缓存信息。过渡发生在各种条件下,有些可能会改变动画参数。过渡上下文对象保证有执行动画的正确信息,缓存的信息可能在掉用动画对象方法时已经过期。

图10-2展示了过渡上下文对象如何与其它对象交互。动画对象在animateTransition:方法中接收该对象。创建的动画发生在提供的容器视图中。例如,弹出视图控制器时,添加它的视图作为容器视图的子视图。容器视图可能是窗口或者一个普通视图,但总是配置来运行动画。

图10-2 过渡上下文对象

关于过渡上下文对象的更多信息,请参考”UIViewControllerContextTransitioning Protocol Reference“。

3.1.4 过渡协调器(Coordinator)

对于内置过渡和自定义过渡,UIKit创建一个过渡协调器对象帮助执行需要的额外动画。除了视图控制器的弹出和关闭,过渡还发生在界面旋转或视图控制器的frame变化时。所有这些过渡都代表视图层级结构的变化。过渡协调器是同时追踪这些变化和让内容产生动画的一种方式。通过受影响的视图控制器的transitionCoordinator属性访问过渡协调器。过渡协调器只存在过渡期间。

图10-3展示了过渡协调器和涉及弹出的视图控制器之间的关系。使用过渡协调器可以获得过渡信息,以及注册想要同时执行的过渡动画的动画块。过渡协调器对象遵循UIViewControllerTransitionCoordinatorContext协议,该协议提供时间信息,动画当前的状态信息,以及过渡中涉及的视图和视图控制器。执行动画块时,它们接收同样信息的上下文对象。

图10-3 过渡协调器

关于过渡协调器对象的更多信息,请参考”UIViewControllerTransitionCoordinator Protocol Reference“。使用配置动画的上下文信息,请参考”UIViewControllerTransitionCoordinatorContext Protocol Reference“。

3.2 使用自定义动画弹出视图控制器

在现有视图控制器的动作方法中完成以下工作,来使用自定义动画弹出视图控制器:

  1. 创建想要弹出的视图控制器。
  2. 创建自定义的过渡代理对象,并分配给视图控制器的transitioningDelegate属性。请求时,过渡代理的方法应该创建并返回自定义动画对象。
  3. 调用presentViewController:animated:completion:方法弹出视图控制器。

调用presentViewController:animated:completion:方法时,UIKit启动弹出过渡。弹出从下一个运行循环迭代开始,一直持续到自定义动画对象调用completeTransition:方法。可交互过渡允许过渡过程中处理触摸事件,而非交互式过渡在动画对象指定的周期内运行。

3.3 实现过渡代理

过渡代理的目的是创建并返回自定义对象。列表10-1展示了过渡方法的可以很简单的实现。例子中创建并返回自定义动画对象。大部分实际工作由动画对象本身处理。

列表10-1 创建动画对象

- (id<UIViewControllerAnimatedTransitioning>)
    animationControllerForPresentedController:(UIViewController *)presented
                         presentingController:(UIViewController *)presenting
                             sourceController:(UIViewController *)source {
    MyAnimator* animator = [[MyAnimator alloc] init];
    return animator;
}

过渡代理的其它方法跟上面例子中一样简单。可以根据应用程序的当前状态合并自定义逻辑,返回不同的动画对象。关于过渡代理方法的更多信息,请参考“UIViewControllerTransitioningDelegate Protocol Reference”。

3.4 实现动画对象

动画对象可以是遵循UIViewControllerAnimatedTransitioning协议的任何对象。动画对象创建在固定时间内执行的动画。动画对象的关键是animateTransition:方法,使用该方法创建实际的动画。动画过程大致分为以下几个部分:

  1. 获取动画参数。
  2. 使用核心动画(Core Animation)或UIView动画方法创建动画。
  3. 清理和完成过渡。

3.4.1 获取动画参数

传递给animateTransition:方法的上下文过渡对象包含执行动画需要的数据。可以从上下文过渡对象中获得最新信息时,永远不要使用缓存信息或者从视图控制器获取信息。弹出和关闭视图控制器有时会涉及视图控制器之外的对象。例如,自定义弹出控制器(presentation controller)可能会添加背景视图作为弹出的一部分。上下文过渡对象会把额外的视图和对象考虑在内,并为动画提供正确的视图。

  • 调用viewControllerForKey:方法两次,获得过渡中的”from“和”to“视图控制器。永远不要认为你知道哪些视图控制器参与过渡。适应新特征环境或者响应应用程序请求时,UIKit可能会改变视图控制器。
  • 调用containerView方法获得动画的父视图。添加所有关键子视图到该视图。例如,弹出过程中,添加presented视图控制器的视图到该视图。
  • 调用viewForKey:方法获得添加或删除的视图。视图控制器的视图可能不是唯一一个在过渡期间添加或删除的视图。弹出控制器可能会在层级结构中插入视图,这些视图也必须添加或删除。viewForKey:方法返回根视图,该视图包含所有需要添加或删除的所有东西。
  • 调用finalFrameForViewController:方法获得被添加或删除视图的最终frame矩形。

上下文过渡对象使用”from“和”to“术语识别过渡中涉及的视图控制器,视图和frame矩形。”from“视图控制器总是在过渡开始时显示在屏幕,”to“视图控制器在过渡结束后显示在屏幕上。如图10-4,”from“和”to”视图控制器在弹出和关闭之间交换位置。

图10-4 “from”和“to”对象

交换值可以编写一个动画对象同时处理弹出和关闭。设计动画对象时,只需要包含一个属性,用来识别是弹出动画还是关闭动画。两者的唯一区别如下:

  • 对于弹出,添加“to”视图到容器视图层级结构。
  • 对于关闭,从容器视图层级结构移除“from”视图。

3.4.2 创建过渡动画

在典型的弹出中,属于presented视图控制器的视图动画的移动到位置上。其它视图可能作为弹出的一部分发生动画,但动画的主要目标总是添加到视图层级结构的视图。

让主视图发生动画时,配置动画的基本动作是一样的。从过渡上下文对象中获取需要的对象和数据,并用这些信息创建实际的动画。

  1. 弹出动画:
  • 使用viewControllerForKey:和viewForKey:方法检索过渡中涉及的视图控制器和视图。
  • 设置“to”视图的起始位置。同时设置其它属性的初始值。
  • 从上下文过渡对象的finalFrameForViewController:方法获得“to”视图的结束位置。
  • 添加“to”视图作为容器视图的子视图。
  • 创建动画。
    • 在动画块中,动画的移动“to”视图到容器视图中的结束位置。同时设置其它属性为最终值。
    • 在完成块中,调用completeTransition:方法,并执行所有其它的清理。
  1. 关闭动画:
  • 使用viewControllerForKey:和viewForKey:方法检索过渡中涉及的视图控制器和视图。
  • 计算“from”视图的结束位置。该视图属于准备关闭的presented视图控制器。
  • 添加“to”视图作为容器视图的子视图。弹出期间,过渡完成后,属于presenting视图控制器的视图会被移除。因此,在关闭操作中,必须添加该视图到容器中。
  • 创建动画。
    • 在动画块中,动画的移动“from”视图到容器视图中的结束位置。同时设置其它属性为最终值。
    • 在完成块中,从视图层级结构中移除”from“视图,并调用completeTransition:方法。根据需要执行清理工作。

图10-5展示了一个自定义弹出和关闭过渡,沿对角线动画移动视图。弹出过程中,presented视图从屏幕外开始,沿对角线向左上角移动直到完全可见。关闭过程中,视图反转方向,沿对角线向右下角移动,直到移出屏幕。

图10-5 自定义弹出和关闭

列表10-2是如何实现图10-5中过渡的代码。检索到动画需要的对象后,animateTransition:方法计算受影响视图的frame矩形。弹出时,toView变量代表presented视图。关闭时,fromView变量代码dismissed视图。presenting属性是动画对象本身的自定义属性,过渡代理创建动画对象时,设置为一个合适的值。

列表10-2 实现沿对角线弹出和关闭动画

- (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 or successful dismissal, remove the view.
                         if ((self.presenting && !success) || (!self.presenting && success)) {
                             [toView removeFromSuperview];
                         }
 
                         // Notify UIKit that the transition has finished
                         [transitionContext completeTransition:success];
                     }];
 
}

3.4.3 动画完成的清理工作

过渡动画结束后,关键要调用completeTransition:方法。调用该方法告诉UIKit过渡完成,用户可能开始使用presented视图控制器。调用该方法还会触发一连串其它完成处理程序,包括presentViewController:animated:completion:方法和动画对象自身的animationEnded:方法。最好在动画块的完成处理程序中调用animationEnded:方法。

因为过渡可能被取消,所以应该使用上下文对象的transitionWasCancelled方法的返回值来决定需要什么清理工作。当取消弹出后时,动画对象必须撤销对视图层级结构做的所有修改。一个成功的关闭也需要类似的动作。

3.5 在过渡中添加交互

让动画可交互最简单的方式是使用UIPercentDrivenInteractiveTransition对象。该对象与现有的动画对象协作控制动画的时间。通过提供的完成百分比完成这项工作。只需要设置事件处理代码,用来计算完成百分比,并在每一个新事件到达时更新百分比。

可以继承或者不继承UIPercentDrivenInteractiveTransition类。如果继承,使用子类的init方法(或者startInteractiveTransition:方法)执行事件处理代码的一次性设置。然后使用自定义事件处理代码计算新的完成百分比,并调用updateInteractiveTransition:方法。当代码确定过渡完成,调用finishInteractiveTransition方法。

列表10-3展示了UIPercentDrivenInteractiveTransition子类的startInteractiveTransition:方法的自定义实现。该方法设置一个拖动手势识别器(pan-gesture recognizer)来追踪触摸事件,并为动画设置该手势识别器到容器视图。同时保存一个过渡上下文的引用以备后用。

列表10-3 配置百分比驱动的可交互动画

- (void)startInteractiveTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
   // Always call super first.
   [super startInteractiveTransition:transitionContext];
 
   // Save the transition context for future reference.
   self.contextData = transitionContext;
 
   // Create a pan gesture recognizer to monitor events.
   self.panGesture = [[UIPanGestureRecognizer alloc]
                        initWithTarget:self action:@selector(handleSwipeUpdate:)];
   self.panGesture.maximumNumberOfTouches = 1;
 
   // Add the gesture recognizer to the container view.
   UIView* container = [transitionContext containerView];
   [container addGestureRecognizer:self.panGesture];
}

手势识别器为每一个到达的心事件调用动作方法。动作方法的实现可以使用手势识别器的状态信息来决定手势是否成功,失败或者仍在进行中。同时,可以使用最新的触摸事件信息来计算手势的新百分比。

列表10-4展示了列表10-3中配置的拖动手势识别器的动作方法。新事件到达时,该方法使用垂直移动距离来计算动画的完成百分比。手势结束时,该方法完成过渡。

列表10-4 使用事件更新动画过程

-(void)handleSwipeUpdate:(UIGestureRecognizer *)gestureRecognizer {
    UIView* container = [self.contextData containerView];
 
    if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {
        // Reset the translation value at the beginning of the gesture.
        [self.panGesture setTranslation:CGPointMake(0, 0) inView:container];
    }
    else if (gestureRecognizer.state == UIGestureRecognizerStateChanged) {
        // Get the current translation value.
        CGPoint translation = [self.panGesture translationInView:container];
 
        // Compute how far the gesture has travelled vertically,
        //  relative to the height of the container view.
        CGFloat percentage = fabs(translation.y / CGRectGetHeight(container.bounds));
 
        // Use the translation value to update the interactive animator.
        [self updateInteractiveTransition:percentage];
    }
    else if (gestureRecognizer.state >= UIGestureRecognizerStateEnded) {
        // Finish the transition and remove the gesture recognizer.
        [self finishInteractiveTransition];
        [[self.contextData containerView] removeGestureRecognizer:self.panGesture];
    }
}

提示:计算的值代表整个动画长度的完成百分比。对于可交互动画,可能希望非线性效果,例如动画本身的初速度,阻尼值和非线性完成曲线。这些效果试图把事件的触摸位置从底层视图的移动中分离。

3.6 创建与过渡并存的动画

过渡中涉及的视图控制器可以在弹出或过渡动画之上执行额外的动画。例如,presented视图控制器过渡中,可能在自身的视图层级结构上执行一些动画,并在过渡发生时添加运动效果或其它可视化反馈。任何可以访问presented或者presenting视图控制器transitionCoordinator属性的对象都可以执行动画。过渡协调器只存在过渡过程中。

调用过渡协调器的animateAlongsideTransition:completion:或者animateAlongsideTransitionInView:animation:completion:方法创建动画。提供的块一直被存储,直到过渡动画开始,此时它们与过渡动画一起执行。

3.7 在弹出控制器(Presentation Controller)中使用动画

对于自定义的弹出,可以提供自己的弹出控制器,给presented视图控制器一个自定义外观。弹出控制器管理独立于视图控制器和它内容的自定义chrome。例如,弹出控制器管理一个放置在视图控制器的视图之后的模糊视图。事实上,弹出控制器不管理特定控制器的视图,所以可以在任何视图控制器中使用相同的弹出控制器。

通过presented视图控制器的过渡代理提供自定义弹出控制器。(视图控制器的modalTransitionStyle属性必须为UIModalPresentationCustom。)弹出控制器与其它动画对象一起运行。动画对象执行视图控制器的视图动画时,弹出控制器执行额外视图的动画。过渡结束时,弹出控制器有机会执行对视图层级结构进行最后的调整。

关于如何创建自定义弹出控制器的信息,请参考“创建自定义弹出”。

4 创建自定义弹出

UIKit分离视图控制器的内容与弹出和显示内容的方式。底层的弹出控制器对象管理presented视图控制器,弹出控制器管理显示视图控制器视图的视觉风格。弹出控制器可能执行以下操作:

  • 设置presented视图控制器的尺寸。
  • 添加自定义视图来改变presented内容的视觉外观。
  • 为它的自定义视图提供过渡动画。
  • 应用程序环境变化时,适应弹出的视觉外观。

UIKit为标准弹出风格提供了弹出控制器。设置视图控制器的弹出风格为UIModalPresentationCustom,并提供一个合适的过渡代理时,UIKit使用自定义弹出控制器。

4.1 自定义弹出过程

弹出一个UIModalPresentationCustom风格的视图控制器时,UIKit查找一个自定义弹出控制器来管理弹出过程。弹出过程中,UIKit调用弹出控制器的方法设置自定义视图,并动画的移动视图到指定位置。

弹出控制器与动画对象一起实现整个过渡。动画对象动画的移动视图控制器的内容到屏幕上,弹出控制器处理其它事情。通常情况下,弹出控制器动画移动自己的视图,但也可以覆写弹出控制器的presentedView方法,让动画对象执行所有或部分视图的动画。

弹出过程中,UIKit:

  1. 调用过渡代理的presentationControllerForPresentedViewController:presentingViewController:sourceViewController:检索自定义弹出控制器。
  2. 向过渡代理请求动画对象和可交互动画对象,如果有的话。
  3. 调用弹出控制器的presentationTransitionWillBegin方法。
    该方法的实现应该添加任意自定义视图到视图层级结构,并配置这些视图的动画。
  4. 从弹出控制器中获得presentedView。
    该方法返回的视图由动画对象动画的移动到指定位置。通常情况下,该方法返回presented视图控制器的根视图。如果需要,弹出控制器可以使用自定义背景视图代替该视图。如果指定了另一个视图,必须把presented视图控制器的根视图嵌入到视图层级结构中。
  5. 执行过渡动画。
    过渡动画包括动画对象创建的主动画,以及配置与主动画一起运行的动画。关于过渡动画的信息,请参考“过渡动画序列”。
    在动画过程中,UIKit调用弹出控制器的containerViewWillLayoutSubviews和containerViewDidLayoutSubviews方法,这样你可以根据需要调整自定义视图的布局。
  6. 过渡动画完成后,调用presentationTransitionDidEnd:方法。

关闭过程中,UIKit:

  1. 从当前可见视图控制器中获得自定义弹出控制器。
  2. 向过渡代理请求动画对象和可交互动画对象,如果有的话。
  3. 调用弹出控制器的dismissalTransitionWillBegin方法。
    该方法的实现应该添加任意自定义视图到视图层级结构,并配置这些视图的动画。
  4. 从弹出控制器获得presentedView。
  5. 执行过渡动画。
    过渡动画包括动画对象创建的主动画,以及配置与主动画一起运行的动画。关于过渡动画的信息,请参考“过渡动画序列”。
    在动画过程中,UIKit调用弹出控制器的containerViewWillLayoutSubviews和containerViewDidLayoutSubviews方法,这样你可以移除自定义约束。
  6. 过渡动画完成后,调用dismissalTransitionDidEnd:方法。

在弹出过程中,弹出控制器的frameOfPresentedViewInContainerView和presentedView方法可能调用多次,因此这些方法的实现应该尽快返回。同时,presentedView方法的实现不应该试图设置视图层级结构。该方法调用时,视图层级结构以及配置好了。

4.2 创建自定义弹出控制器

要实现自定义弹出风格,需要继承UIPresentationController类,并添加创建视图和动画的代码。创建自定义弹出控制器时,考虑以下问题:

  • 想要添加什么视图?
  • 希望如何动画的移动额外的视图到屏幕上?
  • presented视图控制器的尺寸是多少?
  • 弹出如何适应水平方向常规和水平方向紧凑的size class?
  • 弹出完成时,presenting视图控制器的视图是否应该移除?

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

4.2.1 设置Presented视图控制器的Frame

可以修改presented视图控制器的frame矩形,这样它可以值填充可用空间的一部分。默认情况下,presented视图控制器完全填充容器视图的frame。覆写弹出控制器的frameOfPresentedViewInContainerView方法修改frame矩形。列表11-1中,frame修改为只覆盖容器视图的右半边。这种情况下,弹出控制器使用背景模糊的视图覆盖容器的另一半。

列表11-1 改变presented视图控制器的frame

- (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;
}

4.2.2 管理和动画自定义视图

自定义弹出通常涉及添加自定义视图到presented内容。使用自定义视图实现纯视觉装饰,或者使用它们为弹出添加实际行为。例如,背景视图可能包括手势识别器,用来追踪发生在presented内容的bounds之外的特定动作。

弹出控制器负责创建和管理弹出中相关的所有自定义视图。通常在弹出控制器的初始化中创建自定义视图。列表11-2是一个自定义视图控制器的初始化方法,其中创建了自己的模糊视图。该方法创建视图并执行最低限度的配置。

列表11-2 初始化弹出控制器

- (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方法动画的移动自定义视图到屏幕上。在该方法中,配置自定义视图,并把它们添加到容器视图,如列表11-3所示。使用presented或presenting视图控制器的过渡协调器创建任意动画。不要在该方法中修改presented视图控制器的视图。动画对象负责动画移动presented视图控制器到frameOfPresentedViewInContainerView方法返回的frame矩形中。

列表11-3 动画移动模糊视图到屏幕上

- (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:方法完成取消弹出导致的清理工作。如果阀值不满足条件,可交互动画对象可能取消过渡。发生这种情况时,UIKit用NO值调用presentationTransitionDidEnd:方法。取消发生时,移除弹出开始时添加的所有自定义视图,并返回其它视图到上一个配置,如列表11-4所示。

列表11-4 处理取消的弹出

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

视图控制器关闭时,使用dismissalTransitionDidEnd:方法从视图层级结构中移除自定义视图。如果想要视图动画的消失,在dismissalTransitionWillBegin方法设置这些动画。(原文是在dismissalTransitionDidEnd:方法中设置动画,应该是官方文档笔误。如果我说错了,请留言指出。)列表11-5是两个方法的实现,其中移除了上一个例子中的模糊视图。总是需要检查dismissalTransitionDidEnd:方法的参数,查看关闭是成功了还是取消了。

列表11-5 关闭弹出的视图

- (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];
}

4.3 暴露(Vending)弹出控制器给UIKit

弹出视图控制器时,使用自定义弹出控制器执行以下步骤显示视图控制器:

  • 设置presented视图控制器的modalPresentationStyle属性为UIModalPresentationCustom。
  • 为presented视图控制器的transitioningDelegate属性指定一个过渡代理。
  • 实现过渡代理的presentationControllerForPresentedViewController:presentingViewController:sourceViewController:方法。

UIKit需要弹出控制器时,会调用过渡代理的presentationControllerForPresentedViewController:presentingViewController:sourceViewController:方法。该方法的实现应该跟列表11-6这么简单。简单地创建弹出控制器,然后配置并返回。如果该方法返回nil,UIKit使用全屏弹出风格弹出视图控制器。

列表11-6 创建自定义弹出控制器

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

4.4 适应(Adapting)不同的尺寸类(Size Classes )##

当弹出在屏幕上时,底层特征或容器视图尺寸发生变化时,UIKit通知弹出控制器。这些变化通常发生在设备旋转,但也可能在其它时候发生。可以使用特征和尺寸通知来调整弹出的自定义视图,并更新弹出风格。

关于如何适应新特征和尺寸,请参考“创建自适应界面”。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 175,490评论 5 419
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 74,060评论 2 335
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 124,407评论 0 291
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 47,741评论 0 248
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 56,543评论 3 329
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 43,040评论 1 246
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 34,107评论 3 358
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 32,646评论 0 229
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 36,694评论 1 271
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 32,398评论 2 279
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 33,987评论 1 288
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 30,097评论 3 285
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 35,298评论 3 282
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 27,278评论 0 14
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 28,413评论 1 232
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 38,397评论 2 309
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 38,099评论 2 314

推荐阅读更多精彩内容