Slug jesse squires cover

Swift 化的视图控制器展示

UIKit 的一个主要缺点是:视图控制器要完成的工作实在太多了,例如需要完成诸如展示、撤除视图控制器之类的工作。Jesse Squires 谈论了我们该如何借助更 Swift 化的 API,来重新审视以及重新定义这些常见的操作,以便能够减少样板代码的出现,以及提高代码的表现力。


概述 (0:00)

视图控制器无处不在。它们组成了我们应用的核心内容,我们在制作应用的过程中是没有办法离开它们的。然而,在每个视图控制器背后都有一个用于展示的控制器。在本文当中,我会通过一些例子,深入探究控制展示控制器的 API 和 UIKit 本身。接下来,我会用一个更 Swift 化的 API 将其封装起来,让其更加易用。

展示和过渡的对比 (01:07)

过渡 (Transition) 是从一个视图控制器移动到另一个视图控制器的,而展示 (Presentation) 是作为在过渡结束之后的最终样式出现的。它们之间相互依赖,因为展示过程牵涉到了过渡过程。在 UIKit 当中,自定义展示的 API 是挂接到自定义过渡 API 当中的(尽管这并不是必须的;通过自定义展示,您仍然能够提供自定义的过渡)。

展示:UIPresentationController (02:06)

在 iOS 8 中引入的 UIPresentationController API 是非常巧妙的,并且它通常隐藏在幕后。在 iPhone 6 以及 6S 之前,展示基本上是一种只需要在 iPad 上关注的操作而已,而在 iPhone 上这个操作基本是没人关注的。

展示 API 的功能 (03:39)

  • 在展示当中定位您的视图。
  • 在『Chrome』当中管理内容。所谓的内容指的是您的视图控制器的视图,而 Chrome 则是除此之外的一切东西。
  • 自适应:依赖于屏幕分类和环境的变化,或者设备方向等。
  • 展示或者撤销您控制器的动画

展示控制器旨在是能够复用的。它们并没有连接到过渡委托 (Transitioning Delegate) 或者动画对象 (Animator Object) 上面,而是从过渡委托当中返回相关的控制器。

3 个阶段 (05:04)

  1. 展示阶段(移入屏幕)
  2. 管理阶段(您的控制器已经展示完毕,您需要处理可能的旋转操作或者其它环境变化)
  3. 撤销阶段(移出屏幕)

展示和撤销阶段是与过渡相关联的。在使用展示控制器的时候,您可以使用自定义的过渡效果,或者也可以使用默认的过渡效果。

示例 (05:47)

iPad screen presentations diagram

这个图显示了全屏展示 (full screen) 在 iPad 上的情况,以及当控制器与设备宽度相等的页面表单展示 (page sheet) 的情况,还有您可以在 iOS 设置中看到表格表单展示 (form sheet) 的情况。

同样在 iPhone 上面,您可以使用警示控制器 (Alert Controller) 中的动作表单,只是屏幕背后的一种自定义展示。在 iBooks 当中,同样也有一种自定义的弹出框展示控制器。通过警示控制器,我们能够获取相关的展示控制器。这些操作真的是非常的强大、有用。(具体示例参见上方的视频

UIModalPresentationStyle (07:34)

您可以设置模式展示风格 (Modal Presentation Style),例如 .Popover.FullScreen 以及其他风格。UIKit 对这些不同的风格提供了不同的私有 API 类,用来为您设置这些自定义展示控制器:

Receive news and updates from Realm straight to your inbox

// 只需要设置枚举即可!
self.modalPresentationStyle = .FullScreen
self.modalPresentationStyle = .Popover

页面展示与撤销 (08:15)

我们有四种展示视图控制器的方法(我希望有一个就够了!)。showViewControllershowDetailViewController 方法是自适应的展示方法,通常用在分隔视图控制器 (Split View Controller) 的上下文环境当中(您现在可以在 iPhone 上使用了,而不仅仅局限于 iPad)。当您调用这些方法的时候,根据设备是 iPhone 还是 iPad,分隔视图���制器会执行不同的行为。presentViewControllerpushViewController 用于全屏展示控制器。

func presentViewController(_, animated:, completion:)
func pushViewController(_, animated:)
func showViewController(_, sender:)
func showDetailViewController(_, sender:)

我们有两种撤销的方法,这和展示的 API 是不平衡的。我们有四种方式用于展示,但是只有两种方式用于撤销。pushpop 是成对出现的,而其他的展示方式都只能映射到 dismiss 上面了。

func dismissViewControllerAnimated(_, completion:)
func popViewControllerAnimated(_ animated:) -> UIViewController?

UIPresentationController (10:59)

// 当前的 UIViewController API
self.presentationController
self.popoverPresentationController

UIAlertController 是 iOS 8 中新引入的 API,用于替代 UIAlertViewUIActionSheet。它提出了一种完全成熟的视图控制器和自定义展示控制器。和 UISearchController 用于替代 UISearchDisplayController 类似。

UIPopoverPresentationController 是一种明确用于 iPad 和 iPhone 的展示控制器,同时也是一个可自定义的控制器——您必须要首先设置模式风格。随着这些变化,我们有了一个更统一、更一致的 API 来管理视图控制器(比如说它们的目标是什么,以及它们是如何关联到展示控制器上面的)。Popover 控制器在 UIKit 当中是唯一公开可用的自定义展示控制器。它为您的视图控制器提供了特殊的处理。您可以使用一个展示控制器属性,也可以使用一个特定的 Popover 展示控制器属性。如果您拥有一个展示控制器或者 Popover 的话,那么它会在展示控制器当中被返回。

私有 API (12:38)

当您设置这些模式风格的时候,绝大多数风格都会被映射到这个全屏展示控制器当中(在 iPad 上要小一些):

// Modal styles: .FullScreen, .FormSheet, .PageSheet, etc.
_UIFullscreenPresentationController

// For alerts, we have the UI alert controller
// alert presentation controller - Two alerts, two controllers
_UIAlertControllerAlertPresentationController

// On iPhone, it is presented with this ActionSheet compact
// presentation controller; on iPad, an action sheet displays
// as a Popover in certain scenarios
_UIAlertControllerActionSheetCompactPresentationController

// ActionSheet on iPad (Popover)
_UIAlertControllerActionSheetRegularPresentationController

API 总结 (13:58)

  • 有 4 种方法展示视图控制器
  • 通过一个枚举值来设置风格
  • 直接使用 PopoverPresentationController
  • 提供自定义的展示控制器

API 很容易被误用(例如,使用不同的风格和枚举设置了某些不应该被设置的属性),这样 UIKit 可能会抛出异常。为了更好的使用您需要仔细阅读相关文档。

如何使用自定义展示控制器 (15:00)

  1. 建立一个 UIPresentationController 的子类
  2. 通过过渡 API 来提供控制器

1. 子类:过渡与 Chrome (15:21)

您可以对 UI 展示控制器建立一个子类,重写某些方法,然后通过过渡 API 来提供控制器。当您在建立子类的时候,需要重写两个方法(展示过渡和撤销过渡):

func presentationTransitionWillBegin()

func dismissalTransitionWillBegin()

子类:定位 (15:48)

您拥有两种用于定位的方法(分别用于处理子/父视图的尺寸):

func sizeForChildContentContainer(_, withParentContainerSize:) -> CGSize
func frameOfPresentedViewInContainerView() -> CGRect

子类:自适应 (15:48)

func adaptivePresentationStyle() -> UIModalPresentationStyle
func shouldPresentInFullscreen() -> Bool
func containerViewWillLayoutSubviews()

viewWillLayoutSubviews 类似,在 containerViewWillLayoutSubviews 中,您会需要重写这个方法来优雅地建立布局和处理旋转。如果您在使用 AutoLayout 的话,您可能不会遇到困难。如果您设置了相关的约束,那么您可能不需要实现这个方法。

您可以为这个风格或者其他提供的风格返回 .None,比如说 .Fullscreen.Popover 等等,但是这种做法非常局限。我觉得,您应当能够返回任意一个自定义展示控制器。通过这个自定义控制器,您必须要适应环境的变化(例如屏幕分类的变化或者旋转),但是您能进行的唯一方式就是通过内置的风格来适应变化。您无法提供自己的自定义自适应控制器。

2. 提供控制器 (17:24)

要提供控制器的话,您需要实现 UIViewControllerTransitioningDelegate 的 API。展示视图控制器已经实现了这个。您可以在展示了的控制器当中设置这个委托。如果您正位于您的视图控制器当中,实现起来应该如下所示:

let vc = MyViewController()
vc.transitioningDelegate = self

您可以使用这个来查看它的路径方向(从哪个控制器到哪个控制器),然后返回正确的展示控制器。

func presentationControllerForPresentedViewController(_,
    presentingViewController:,
    sourceViewController:) -> UIPresentationController?

样例 (18:30)

要想出我们能从 Swift 化的 API 中能获取到些什么,请参阅视频中的本节部分。我做了一个关于 Push、Modal、Show/ShowDetail、Popover、PopoverFrontView 以及 Custom 视图展示的样例。

通过这个 API,比起原本在内含的视图控制器中进行控制要好得多。在以前,您将不得不增加一个子视图控制器,并且自行管理这个过渡效果,以及在您的视图层级当中对这些子视图进行布局。当您需要实现此类功能的时候,比起在内含的视图控制器中进行控制来说,使用展示控制器是个更好的选择。

让我们构建一个小型库 (21:00)

enum PresentationType {
    case Modal(NavigationStyle, UIModalPresentationStyle, UIModalTransitionStyle)
    case Popover(PopoverConfig)
    case Push
    case Show
    case ShowDetail(NavigationStyle)
    case Custom(UIViewControllerTransitioningDelegate)
}

对于 NavigationStyle 枚举来说,则有两个元素:None 或者 WithNavigation

enum NavigationStyle {
    case None
    case WithNavigation
}

您可能遇到这种情况:您有一个视图控制器,并且希望将其 push 到导航栈当中。您可以轻松得到 Chrome,获取返回按钮,并且由于您是将其 push 到一个已存在的栈当中,因此您不需要将其嵌入到导航控制器当中。如果您想要用 modal 方式将其展示的话,您仍需要一个带有标题和取消按钮的导航栏放在导航控制器当中,然后用 modal 方式将其 pop 出来。Popovers 可以从两个区域中展示:栏目按钮 (Bar button Item) 或者其他任意一类视图。UIBarButtonItem 不属于 UIView。如果您需要展示 Popover ,但是又没有配置正确的话,您会得到一个异常。这个结构体将包含有源 (Source)、ArrowDirection 以及一个可选的委托,用以配置 Popover。

struct PopoverConfig {
    enum Source {
        case BarButtonItem(UIBarButtonItem)
        case View(UIView)
    }

    let source: Source
    let arrowDirection: UIPopoverArrowDirection
    let delegate: UIPopoverPresentationControllerDelegate?
}

所有这些东西都会映射到这个新方法当中 (这个添加的方法用来查看我们的视图控制器)。这和我们用以展示视图控制器的方法类似,除了现在我们会用 PresentationType 进行控制。我们只有这一个入口点,也就是只有一个方法调用(参见下面)。您将会在 PresentationType 枚举中映射这些配置数据,然后将其回传到 UIKit 的属性和方法当中。

extension UIViewController {

// maps PresentationType to UIKit properties and methods
func presentViewController(
                     controller: UIViewController,
                     type: PresentationType,
                     animated: Bool = true)
}

用法 (24:44)

如果我们想要 push 一个视图控制器的话:

presentViewController(vc, type: .Push)

如果我们想要使用一个自定义的 Popover 的话:

let config = PopoverConfig(source: .View(v))
presentViewController(vc, type: .Popover(config))

Modal 的情况:

let type = .Modal(.WithNavigation, .FullScreen, .CoverVertical)
presentViewController(vc, type: type)

通过我们的自定义类型,显然我们可以使用这个自定义控制器:

presentViewController(vc, type: .Custom(self))

self 将是您所在的视图控制器(它会调用一个用以返回您的自定义控制器的方法)。我很乐意将自定义枚举赋给实际的控制器本身,但是通过这个 API 您无法直接设置展示控制器。事实上,它们都是只读的。当您从过渡方法中返回它们的时候,这就是可以通过此来设置这些属性的机制。

Swift 化的 API (26:15)

这个 API 更具有表现力、更加简洁。您在这个 API 中只有一个单一的入口点:这更加结构化,而且不容易被滥用。枚举迫使我们必须要正确地对其进行配置。

额外资源 (26:36)

问题时间到! (27:40)

问:如果要在自定义展示控制器中处理屏幕类别改变的话,您对此有没有什么好的解决方案呢?您是否需要读取它是在常规 (regular) 状态还是在紧缩 (compact) 状态?

Jesse:您可以实现一些方法来解决这个问题(在我的幻灯片上我漏掉了一个),它将会传入到特征集合 (trait collection) 当中,这将是您需要重写的方法。在可能会存在于父级类中的 Popover 控制器上,有不少的委托方法。您可以使用它们进行关联,这样就可以处理自适应相关的问题了。

问:您将如何与过渡委托 (transitioning delegates) 进行交互?

Jesse:这是一个不同的部分。在 UIView 过渡委托 API 的一部分当中,您可以返回这些在交互式动画中的动画对象。我们只用关注那个返回展示控制器的方法即可。在这个委托协议当中用以处理过渡 API 的其他方法——它们会正常工作的。展示控制器将会管理这个单独的区域,它们之间将会很好地协同工作,但是却不会以任何方式进行结合。有一件事我忘了提,通过这个 half modal 展示空格逆止器,我们将使用的是内置的过渡风格。通过 modal 过渡风格枚举,或者 modal 展示风格枚举,您可以得到一个对应的过渡风格:有垂直覆盖(cover vertical)、 交叉溶解(across dissolve),以及类似于水平翻转的效果 (我觉得)。UIKit 将会提供自定义的动画对象。通过我们的 half modal,它将会通过垂直覆盖动画进行展示,也就是从底部出现的动画。我们可以轻松地实现交叉溶解,并且使用内置在 UIKit 当中 的风格。您可以随意将它们进行揉合。

问:您提到了内含 API (containment API)。我在 iOS 5 中使用过这些内含 API。我们看到在展示控制器当中,似乎 API 太多了。我很惊讶地听到您说这个要好用很多;有没有一种情况,使用内含 API 会更好一些?我觉得在这个新的展示控制器中所提及的 API 对于处理屏幕分类来说是很复杂的,并且如果您需要前往一个有复杂上下文环境的视图控制器当中的某个视图控制器的话,这也不是一个很好的选择。不过我觉得在静态展示视图控制器以及制作自定义的导航条来说更有用一些。

Jesse:没错,在汉堡包菜单 (hamburget menu) 中使用展示控制器是一个最好的例子。

问:是否有一种情况会导致您回归使用内含 API,或者即便如此,您还是坚持使用展示控制器?

Jesse:这取决于您的看法。我们用 App Store 这个应用作为例子。顶部的 banner 图,您必须要为不同分类中的热门应用建立不同的部分。必须要始终能够横向滚动这些部分。我会为每个在这里的集合视图使用内含的视图控制器 API;它们每个都有自己的子视图控制器(您没办法使用大规模的视图控制器进行管理)。根控制器将管理所有的子控制器,并且当您在相同层级的时候,使用内含 API 是正确的选择。但是,当您想要展示额外的或者临时的视图的时候,比起常见的使用内含 API 来添加这些额外或者临时的视图,例如选择选项之类的操作,使用展示控制器来处理是个更好的选择。在这个例子中,我们可以将背景变暗,然后防止用户与之交互。不过,您可以对其进行自定义,让两个控制器能够同时进行交互。您可以在它们之间搭建委托,这样就可以在两个控制器之间传递数据。这样做的好处是:Popover 或者展示控制器将可以重复使用。您可以将任何视图控制器放到展示控制器当中(反之亦然)。它可以帮助您在您的应用程序中建立一些可重用的组件。另一方面,内含 API 可以更紧密地结合这些不同的控制器和视图。

About the content

This content has been published here with the express permission of the author.

Jesse Squires

Jesse is an iOS developer at Instagram who writes about Swift and Objective‑C on his blog at jessesquires.com. He’s the curator of the Swift Weekly Brief newsletter and co-host of the Swift Unwrapped podcast. He is fueled primarily by black coffee and black metal.

4 design patterns for a RESTless mobile integration »

close