视图控制器是应用程序数据和其视觉外形之间的一个至关重要的链接。无论何时,应用程序显示一个用户界面,其显示的内容都是由一个或一组互相合作的视图控制器管理。因此,视图控制器给你建立的应用程序提供了骨架。
iOS提供了很多内置的视图控制器类来支持标准用户界面块(piece),比如导航和标签栏。作为开发应用程序的一部分,你还可以实现一个或多个自定义控制器来显示应用程序的特定内容。
在模型-视图-控制器(MVC)设计模式里,视图控制器是传统的控制器对象,但是它们的作用常常还不止这些。视图控制器给所有iOS应用程序提供了很多通 用行为。这些行为常常被内建在基础类之内。对于一些行为,基础类提供了部分解决方法,你的视图控制器子类实现自定义代码来提供其余的解决方法。 比如,当用户旋转设备,标准实现尝试去旋转用户界面;然而,你的子类决定用户界面是否应该被旋转,并且如果旋转,其视图在新方向上应该如何改变其配置。因 此,当遵循平台设计指南时,一个结构性基础类和具体子类挂钩相结合让自定义应用程序行为变得简单。
视图控制器管理应用程序用户界面的一个独立部分。根据请求,它提供一个可以被显示或能与之交互的视图。该视图常常是为一个更复杂的视图层次结构提供的根视 图--按钮,开关,以及其它在iOS中已经有实现的用户界面元素。视图控制器就是该视图层次结构的中央协调机构,处理视图和任何相关控制器或数据对象的交 换。
要想在应用程序里呈现特殊的内容,你可以实现你自己的内容视图控制器。你可以通过子类化 UIViewController 或 UITableViewController 类来创建新的视图控制器,并实现必要的方法来呈现并控制你的内容。
容器视图控制器显示其它视图控制器拥有的内容。这些其它视图控制器明确地与容器相关联,形成了一种父子关系。容器和内容视图控制器的组合创建了一个视图控制器对象的层次结构,其中只有一个单一的根视图控制器。
容器的每种类型定义了它自己的界面来管理它的子视图控制器。容器方法有时候在子视图控制器之间定义了具体的导航关系。容器还能对其子视图控制器的类型设置特定限制。它还可能期待其子视图控制器来提供额外的内容以用于配置容器。
iOS提供了很多内建的容器视图控制器类型,你可以使用它们来组织你的用户界面。
有时候视图控制器想给用户显示额外的信息。或者它想要用户提供额外的信息或执行一个任务。在iOS设备中的屏幕控件很有限;设备可能没有足够的空间来同时显示所有的使用界面元素。相反,iOS应用程序暂时显示另一个视图来让用户与之交互。视图只显示到用户结束请求动作。
为了简化实现这样的界面所需的努力,iOS允许视图控制器呈现另一个视图控制器的内容。当你呈现内容时,视图控制器的视图被显示在屏幕的一部分--常常是 整个屏幕。 然后,当用户完成任务后,被显示的视图控制器告诉视图控制器呈现任务已经完成。呈现的视图控制器然后释放它呈现的视图控制器,把屏幕恢复到它的初始状态。
呈现行为必须按顺序包含在一个视图控制器设计中,这样它们就可以被另一个视图控制器呈现。
用户界面设计可以非常复杂。每个视图控制器引用多个视图,手势辨认者(gesture recognizers),以及其它用户界面对象。作为回报,这些对象保持对视图控制器的引用或执行指定代码片段以响应用户所做的动作。而且视图控制器很 少单独地采取行动。多个视图控制器之间的协作还定义了应用程序中的其它关系。就是说,创建一个用户界面就是实例化和配置很多对象并建立它们之间的关系,而 这将很耗时并容易发生错误。
作为替换,使用界面生成器(Interface Builder)来创建故事板(storyboards)。故事板持有视图控制器的预配置实例以及它们的相关对象。每个对象的属性都可以在界面生成器里配置,它们之间的关系也可以。
在运行时,应用程序加载故事板并使用它们来管理应用程序的界面。当对象从故事板加载时,它们都被恢复到你在故事板中配置的状态。UIKit还提供了很多你可以加载的方法,你可以用它们来自定义不能直接在界面生成器中配置的各种行为。
通过使用故事板,你可以很容易看到的对象是如何在你的应用程序的用户界面结合在一起。你还可以用少量代码就创建并配置应用程序的用户界面对象。
从阅读 “View Controller Basics,” 开始,它解释了视图控制器如何创建应用程序界面。然后,阅读 “Using View Controllers in Your App” 来理解如何使用视图控制器,包括哪些内建在iOS中的视图控制器以及你自己创建的视图控制器。
当你准备实现应用程序的自定义视图控制器时,阅读 “Creating Custom Content View Controllers” 来了解视图控制器指定的所有任务,然后阅读该文档中的剩余章节来学习如何实现那些行为。
在开始阅读本文档之前,你应该先熟悉 Start Developing iOS Apps Today 和 Your Second iOS App: Storyboards 中的内容。故事板教程演示了很多在本书中描述的技术,包括以下Cocoa概念:
定义新的Objective-C类
在管理应用程序行为中委托对象的作用
模型-视图-控制器范例
关于UIKit提供的标准容器视图控制器的更多信息,请看 View Controller Catalog for iOS 。
关于如何在视图控制器中操作视图的指南,请看 View Programming Guide for iOS .
关于如何在视图控制器中处理事件的指南,请看 Event Handling Guide for iOS .
关于iOS应用程序的整个结构的更多信息,请看 iOS App Programming Guide .
关于如何在项目中配置故事板的指南,请看 Xcode User Guide 。
运行在基于iOS设备的应用程序只有有限的屏幕空间来显示内容,因此它们如何给用户呈现信息必须有创造性。那些有很多信息需要显示的应用程序在开始时因此 只能显示一部分,然后当用户跟应用程序交互时显示和隐藏额外的内容。视图控制器对象为管理内容和协调内容的显示和隐藏提供了基础设置。通过拥有不同的视图 控制器类控制用户界面的单独部分,你把用户界面的实现分成几个更小更容易管理的单元。
在你能在应用程序中使用视图控制器之前,你需要对在iOS应用程序中用来显示内容的主要类有一个基本的理解,包括窗口和视图。任何视图控制器实现的关键部 分是管理显示内容的视图。 但是,管理视图不是视图控制器执行的唯一工作。当过渡发生时,大多数视图控制器还跟其他视图控制器交流和协调。因为视图控制器管理着很多链接,包括内部的 视图和相关对象的链接以及外部的其它控制器之间的链接,理解对象之间的各种链接有时候很困难。作为替代,使用界面生成器来创建故事板。故事板让应用程序中 的关系的可视化更简单并大大简化初始化对象在运行时所需的努力。
下图显示了一个简单的界面。左侧,你可以看见组成该界面的各种对象并理解它们是如何互相连接的。
这里有三个主要对象在工作:
一个 UIScreen 对象表示连接到设备的一个物理屏幕。
一个 UIWindow 对象为屏幕提供绘图支持。
一组 UIView 对象来执行绘图。这些对象被连接到窗口,当窗口要求它们绘制内容时绘制。
下图显示了这些类(以及相关重要类)在UIKit中如何被定义。
尽管你不需要理解关于视图的所有知识,进而理解视图控制器,但是它在考虑视图的最显著功能上很有帮助:
一个视图表示一个用户界面元素。每个视图覆盖了一个特定的区域。在那个区域中,它显示内容或响应用户事件。
在一个视图层次结构里视图可以被嵌套。子视图相对于它们的父视图定位和绘制。因此,当父视图发生移动,其子视图也跟着移动。该层次结构通过把它们放在一个通用父类中让集合一组相关视图变得容易。
视图可以动画它们的属性值。当一个属性值的改变发生动画时,该属性值在定义的时间区内逐渐改变直到它达到目标新值。交叉在多个视图的多个属性的改变可以在一个动画里协调动画。
动画对于iOS应用程序开发是至关重要的。因为大多数应用程序在一个时间只显示内容的一部分,动画允许用户看到一个过渡何时发生以及新内容从哪来。一个瞬间过渡可能让用户困惑。
视图很少理解它们在应用程序中扮演的角色。比如,图1-1显示了一个按钮(标题Hello), 它是视图的一种特殊形式,被称为控件。控件知道如何响应用户在该区域的交互,但是它们不知道它们该控制什么。相反,当用户跟控件发生交互时,它给应用程序 中的其它对象发送消息。 该灵活性允许一个单类( UIButton )提供多个按钮的实现,每个都能触发一个不同的操作。
一个复杂的应用程序需要很多视图,这些视图都被组合进视图层次结构里。它需要动画这些视图的子集到屏幕或使其离开屏幕来提供一个更大界面的错觉。最后,保 持视图类可重用,视图类需要对它们在应用程序中执行的特殊角色一无所知。所以应用程序逻辑---大脑---还需要被放在其它地方。视图控制器就是把应用程 序的视图联系到一起的大脑。
每个视图控制器都组织管理一个视图;该视图常常是一个视图层次结构中的根视图。视图控制器就是MVC模式里的控制器对象,但是视图控制器还有特殊任务iOS期望它去执行。这些任务由 UIViewController 类定义,所有视图控制器都从它这继承。所有视图控制器执行视图和资源管理任务;其它职责取决于如何使用视图控制器。
图 1-3 一个附加到窗口的视图控制器自动添加其视图到窗口,使它们成为窗口的子视图
视图控制器只在视图被需要时才小心的加载该视图。它还能在特定条件下释放视图。因为这些原因,视图控制器在管理应用程序中的资源方面起着关键作用。
视图控制器是协调其所有被管理视图的行为的原始地方。比如,当一个按钮被按下,它给视图控制器发送了一个消息。尽管视图本身可能不知道它执行的任务,但是 视图控制器应该知道按钮按下意味着什么,以及它该如何响应。控制器可能更新数据对象,动画或改变存储在其视图中的属性值,或甚至把另一个视图控制器的内容 显示到屏幕。
通常,应用程序实例化的每个视图控制器只显示应用程序数据的一个子集。它知道如何显示那个特殊的数据集,而不需要了解其它类型的数据。因此,一个应用程序数据模型,用户接口设计,以及你创建的视图控制器都互相影响。
图 1-4 显示了一个管理食谱的应用程序。该应用程序显示了三个相关但是独立的视图。 第一个视图列出了应用程序管理的食谱。点击一个食谱显示第二个视图,它描述了选中的视图。点击详细视图中的食谱图片,打开第三个视图,一张放大的图片。每 个视图都有一个独立的视图控制器对象管理---呈现适当的视图,用数据填充子视图,以及在视图层次结构里响应用户交互。
该例子说明了视图控制器的一些常见要素:
每个视图都只被一个视图控制器管理。当一个视图被分配给视图控制器的 view 属性,视图控制器就拥有了该视图。如果视图是一个子视图,它可能被同一个或不同的视图控制器控制。当你学习容器视图控制器(container view controllers)时,你将学习到更多关于如何使用多个视图控制器来组织一个单一的视图层次结构。
每个视图控制器跟应用程序数据的一个子集发生交互。比如,Photo controller只需要知道要显示的照片。
因为每个视图控制器只提供用户体验的一个子集,视图控制器必须互相交流来让该体验完美衔接。它们还可能跟其它控制器相交流,比如数据控制器或文档对象。
图1-5 显示了UIkit框架中可用的视图控制器类,以及其它对视图控制器很重要的类。比如,UITabBarController 对象管理一个UITabBar对象,UITabBar对象实际上显示了跟标签栏界面相关的标签。 其它框架定义的额外的视图控制器类没有显示在该图中。
图1-5 UIKit中的视图控制器类
视图控制器,不管是那些由iOS提供的还是你自己定义的视图控制器,都可以被分为两个基本类别----内容视图控制器和容器视图控制器---它们反应了视图控制器在应用程序中所扮演的角色。
内容视图控制器使用一个视图或一个由一组视图组成的视图层次结构在屏幕上呈现内容。之前描述的控制器已经有内容视图控制器。一个内容视图控制器通常知道应用程序数据的子集,该数据跟控制器在应用程序中所扮演的角色有关。
以下列出了应用程序使用内容视图控制器的常用地方:
给用户显示数据
从用户那收集数据
执行一个指定任务
在一组可行命令或选项之间导航,比如一个游戏的启动屏幕
内容视图控制器是应用程序的主要协调对象,因为它们知道数据和应用程序提供给用户的任务的具体详情。
你创建的每个内容视图控制器对象负责管理一个单一视图层次结构中的所有视图。一个视图控制器和其视图层次结构中所有视图之间的一对一对应关系是主要的设计考虑。你不应该使用多个内容视图控制器来管理同一个视图层次。同样的,你也不应该使用一个单一内容视图控制器对象来管理多个屏幕上的内容价值。
关于定义你的内容视图控制器以及实现所需的行为的信息,请看 “Creating Custom Content View Controllers.”
很多应用程序显示表格数据。因此,iOS提供了一个专门用来管理表格数据的内建的UIViewController类的子类。 UITableViewController 管理一个表格视图并支持很多标准表格相关的行为,比如选择(selection)管理,行编辑,以及表格配置。这些额外的支持减少了你创建和初始化一个基于表格界面必须编写的代码总量。你还可以子类化UITableViewController来添加其它自定义行为。
图1-6 显示了一个使用表格视图控制器的例子。因为它是 UIViewController 的一个子类,表格视图控制器任然有一个指向接口根视图的指针(通过其 view 属性),但是它还有一个指向界面中显示的表格视图的独立指针。
容器视图控制器包含了其它视图控制器拥有的内容。这些其它视图控制器都明确地被作为其子视图控制器分配给容器视图控制器。一个容器控制器可以是其它控制器的父控制器也可以是另一个控制器的子控制器。最终,这些控制器的组合建立一个视图层次结构。
每种类型的容器视图控制器建立了一个让其子控制器能在里操作的用户界面。该用户界面的视觉呈现,以及它施加给子控制器的设计可以在不同类型的容器之间广阔地变化。比如,以下有一些方法让不同的容器视图控制器可以区分它们自己:
一个容器提供它自己的API来管理其子控制器。
一个容器决定子控制器之间是否有关系,以及什么关系。
一个容器跟其它视图控制器一样管理一个视图层次。一个容器还可以添加子控制器的任何视图到它的视图层次。容器决定何时添加这样的一个视图,以及它应该如何被调整尺寸以适应容器的视图层次结构,但是除此之外子视图控制器保留了对视图和其子视图的职责。
一个容器可能施加特定设计构思到其子控制器。比如,一个容器可能一些特定的视图控制器类限制其子控制器,或它可能期望那些控制器来提供额外的内容来配置容器的视图。
每个内建的容器类都围绕着一个重要的用户界面原则来组织的。你可以使用由这些容器管理的用户界面来组织复杂的应用程序。
导航控制器呈现按层次组织的数据,它是 UINavigationController 类的一个实例。该类的方法对管理一个基于栈的内容视图控制器集合提供支持。该栈表示用户通过层次数据获取的路径,栈的末端反应起始点,栈的顶端反应用户在数据中的当前位置。
图1-7 显示了通讯录应用程序的屏幕,它使用一个导航控制器来呈现联络信息给用户。每个页面顶部的导航栏是导航控制器所有。每个屏幕显示给用户的其余部分是由一个 内容视图控制器管理,内容视图控制器显示数据层次中特定层的信息。当用户在界面中跟控件发生交互,那些控件告诉导航控制器显示序列中的下一个视图控制器或 丢弃当前的视图控制器。
尽管一个导航控制器的主要任务是管理它的子视图控制器,但是它还管理一些视图。特别是,它管理一个导航栏(显示用户在数据层次中的当前位置信息),一个按钮(导航到前一个屏幕),以及当前视图控制器需要的任何自定义控件。你不能直接修改视图控制器拥有的视图。作为替代,你配置控件,这些控件由导航控制器显示,它们通过设置在每个子视图控制器上的属性来显示。
关于如何配置和使用导航控件对象的信息,请看 “Navigation Controllers” .
标签栏控制器是一个容器视图控制器,你可以用它来把应用程序分成两个或更多不同的操作模式。标签栏控制器是 UITabBarController类的一个实例。标签栏有多个标签,每个表示一个子视图控制器。选择一个标签导致标签栏控制器在屏幕上显示相关视图控制器的视图。
图 1-8 显示了Clock应用程序的集中模式,以及相关视图控制器之间的关系。 每个模式都有一个内容视图控制器来管理主要内容区域。 在Clock应用程序中,Clock 和 Alarm 视图控制器都显示一个导航风格的界面来沿着屏幕的顶部容纳一些额外的控件。其它模式使用内容视图控制器来呈现一个单一屏幕。
当你的应用程序呈现不同种类数据或者以不同方式呈现相同数据的时候,你可以使用标签栏控制器。
关于如何配置和使用一个标签栏控制器的信息,请看 “Tab Bar Controllers” .
拆分视图控制器把屏幕分成多个部分,每个部分都可以被单独更新。一个拆分视图控制器的外形可能很大程度上依赖于它的方向。拆分视图控制器是 UISplitViewController 类的一个实例。一个拆分视图界面的内容来源于两个子视图控制器。
图1-9显示了 MultipleDetailViews 例子应用程序中的一个拆分视图界面。在竖直模式,只有详细视图被显示。列表视图用一个弹出菜单(popover)打开。然而,当屏幕处于水平模式时,拆分视图控制器一边一个显示两个子控制器的内容。
拆分视图控制器仅在iPad上支持,旨在帮助您利用该设备的大屏幕。它们是iPad应用程序中实现主-细节界面的首选方法。
关于如何配置和使用一个拆分视图控制器的信息,请看 “Popovers” .
再次看图1-9,当拆分视图控制器以竖直模式显示时,主视图在一个特殊的控件中显示,被称为popover。在一个iPad应用程序中,你可以使用popover控制器( UIPopoverController
) 来实现弹出控件。
弹出控制器实际上不是一个容器;它不从 UIViewController 继承。但是,实践中,弹出控制器跟容器相似,所以你在使用它们时可以应用相同的编程原则。
页面视图控制器是一个容器视图控制器,它用来实现一个页面布局。那个布局允许用户翻转内容的不同页,就好像翻书一样。页面视图控制器是 UIPageViewController类的一个实例。每个内容页由一个内容视图控制器提供。页面视图控制器管理页面之间的过渡。当需要新页面时,页面上hi图控制器调用一个相关的数据源来为下一个页面取回一个视图控制器。
关于如何配置和使用一个页面视图控制器的信息,请看 “Page View Controllers” .
要想把一个视图控制器的内容显示给用户,它必须关联一个窗口。你可以在应用程序中使用以下方法来实现:
把视图控制器作为窗口的根视图控制器
把视图控制器作为一个容器的子控制器
在一个弹出控件中显示视图控制器
从另一个视图控制器中呈现它
图1-10 显示了Contacts应用程序的一个例子。当用户点击加号来添加一个新联系方式时,Contacts视图控制器呈现添加联系人视图控制器。添加联系人屏幕保持可见直到用户取消操作或为联系方式提供了足够的信息让其保存到联系人数据库中。在那时信息被发送给联系人视图控制器(Contacts view controller),然后丢弃呈现的控制器。
被呈现的视图控制器没有指定类型---可以是内容视图控制器也可以是带有一个内容视图控制器的容器视图控制器。 实践中,内容视图控制器是专门设计为能让另一个控制器呈现,因此把它想象成一个内容视图控制器的变量(variant)会很有帮助。尽管容器视图控制器定义了被管理控制器之间的特定关系,但是使用presentation允许你定义被呈现的视图控制器和呈现它的视图控制器之间的关系。
大多数时间,你呈现视图控制器来收集用户信息或为一些特定目的捕捉用户的注意力。一旦那个目的完成,presenting视图控制器就丢弃被呈现的视图控制器并返回到标准应用程序界面。
一个被呈现的视图控制器本身还可以呈现另一个视图控制器,这点值得注意。 当你需要线性的执行多个模型动作时,这个把视图控制器链接到一起的功能会很有用。比如,如果用户点击点击图1-10中的New Contact屏幕上的添加照片按钮,想要选择一张已经存在的图片,New Contact视图控制器呈现一个图片选择器界面。用户必须丢弃图片选择器屏幕,然后单独地丢弃New Contact屏幕返回到联系人列表。
当呈现一个视图控制器时,一个视图控制器决定花费多少屏幕来呈现视图控制器。屏幕部分默认被称为呈现上下文(presentation context), 呈现上下文被定义用来覆盖窗口。
关于如何在应用程序中呈现视图控制器的更多信息,请看 “Presenting View Controllers from Other View Controllers.”
视图控制器管理它们的视图以及其它相关对象,但是它们还跟其它视图控制器一起提供一个无缝的用户界面。您的应用程序的视图控制器之间的工作和交流的分配是与它们一起工作的一个重要组成部分。因为这些关系对于建立复杂应用程序是如此重要,下一节回顾已经讨论过的关系,并介绍了它们的更多细节。
一个视图控制器层次结构以一个单一父控制器开始,即窗口的根视图控制器。如果那个视图控制器是是一个容器,它可能有提供内容的子控制器。那些子控制器相应 地也可能是它们自己的子控制器的容器。图1-11 显示了一个视图控制器层次结构。根视图控制器是一个带有4个标签的标签视图控制器。第一个标签使用一个带有自己的子控制器的导航控制器,其它三个标签由内 容视图控制器管理,它们没有子控制器。
每个视图控制器填充的区域由其父控制器决定。根视图控制器的区域是由窗口决定的。在图1-11中,标签视图控制器从窗口获取它的尺寸。它给标签栏留出空 间,并把剩余空间给子视图。如果导航控制器是刚刚显示的控件,它给导航栏留出控件,并把剩余空间留给内容控制器。在每个步骤,子视图控制器的视图由父视图重新调整尺寸并放入父视图的视图层次结构中。
该视图和视图控制器的组合还为应用程序处理事件建立了响应链。
容器的类型定义了其子视图控制器共享的关系(如果存在)。比如,比较标签视图控制器和导航控制器。
在一个标签视图控制器中,标签标示内容的独立屏幕;标签栏控制器没有给它的子视图控制器定义一个关系,尽管你的应用程序可以这么做。
在一个导航控制器中,显示同胞的相关视图被安排在一个栈中。同胞常常跟相连的同胞共享一个连接。
图1-12 显示了跟导航控制器有关的一个通用视图控制器配置。第一个子控制器,master,显示了可用的内容,不显示所有的细节。当一个数据项被选择,它推送一个新同胞到导航控制器,这样用户就能看到额外的细节。相似地,如果用户需要查看更多细节,该同胞可以推送另一个视图控制器来显示可用的最详细内容。当同胞们有像该例子中的一个良好定义,它们常常直接或通过容器控制器互相协作。参见 Figure 1-15 .
当一个视图控制器想要另一个视图控制器执行一个任务时,它呈现另一个视图控制器。呈现它的视图控制器管理该行为。它配置被呈现的视图控制器,从它那接收信息,并最终丢弃它。然而,当它被呈现时,被呈现的视图控制器的视图暂时被添加到窗口的视图层次结构中。
图1-13, 一个连接到标签视图(Tab controller)的内容视图(Content)呈现了一个视图控制器(Modal)来执行一个任务。Content是呈现视图控制器,Modal视图控制器是被呈现的视图控制器。
当一个视图控制器被呈现时,它覆盖的一部分屏幕由另一个视图控制器提供的呈现上下文(presentation context)定义。提供呈现上下文的视图控制器不需要跟呈现它的视图控制器是同一个。图1-14显示了跟图1-13同样的视图控制器层次。你可以看到内容视图(content view)呈现了视图控制器,但是它不提供呈现上下文。 相反,视图控制器由标签控制器所呈现。因此,即使呈现的视图控制器只覆盖了标签视图控制器提供给它的部分屏幕,但是被呈现的视图控制器任然使用了标签视图控制器所拥有的整个区域。
图1-14 实际呈现由根视图控制器执行
拥有多个视图控制器的应用程序,视图控制器的创建和销毁通常贯穿于应用程序的整个生命期。在生命期期间,视图控制器互相交流以呈现一个无缝的用户体验。这些关系代表应用程序的控制流。
这样的控制流最常见的发生时间是当一个新的视图控制器被实例化时。通常,一个视图控制器被实例化是因为另一个视图控制器中的动作。第一个视图控制器,被称为源视图控制器引出第二个视图控制器,即目标视图控制器。如果目标视图控制器需要给用户呈现数据,源视图控制器常常提供那些数据。相似地,如果源视图控制器需要从目标视图控制器获取信息,它负责建立两个视图控制器之间的连接。
图1-15 显示了这些关系的最通用例子
在图中:
一个导航控制器的一个子控制器推送另一个子控制器到导航栈。
一个视图控制器呈现另一个视图控制器。
一个视图控制器在一个弹出菜单(popover)里显示另一个视图控制器。
每个控制器都有前一个控制器所配置。当多个控制器在一起工作,它们建立一个贯穿整个应用程序的交流链。
链中每个链接的控制流都由目标视图控制器定义。源视图控制器使用目标视图控制器提供的各种约定。
目标视图控制器提供用来配置其数据和呈现的各种属性。
如果目标视图控制器需要跟链中前一个视图控制器交流,它使用委托。当源视图控制器配置目标视图控制器的其它属性,它还应该提供一个实现委托协议的对象。
使用该控制流约定的好处是它让每对源和目标视图控制器之间有一个清楚的责任分工。当源视图控制器要求目标视图控制器执行一个任务时,数据沿着路径往下流动;源视图控制器驱动该进程。比如,它可能提供目标控制器应该显示的特定数据。在一个方向,当一个视图控制器需要把交流信息传回催生它的源控制器时,数据沿着路径向上流动。比如它可能在任务完成时发生交流。
此外,通过持续实现这种控制流模型,你还可以确保目标视图控制器绝不会知道关于源视图控制器的太多配置信息。即使它确实了解关于链中的前一个视图控制器,它也只知道实现委托协议的类,而不是类中的类。为了防止视图控制器之间了解太多,让控制器们独立变得更加重要。对于阅读你的代码的其他人,一个持续实现的控制流模型让它能很容易就看到任何一对控制器之间的交流路径。
当你用故事板实现你的应用程序时,你可以使用界面生成器来组织你的应用程序视图控制器以及任何相关视图。图1-16显示了界面生成器上的一个界面。 Interface Builder中的可视化布局一目了然,让您可以了解您的应用程序中的流。你可以看到哪些视图控制器是由应用程序实例化的,以及它们被实例化的顺序。 更重要的是,你可以配置视图的复杂集合以及故事板中的其它对象。由此产生的故事板作为一个文件被存储在项目中。当你建立项目时,项目中的故事板文件被处理并被拷贝到应用程序束(bundle)中,在那里它们在运行时由应用程序加载。
通常,iOS可以根据需要在故事板中自动实例化视图控制器。 相似地,跟每个控制器相关的视图层次在它需要被显示时自动被加载。视图控制器和视图都以界面生成器中的相同配置被实例化。因为大多数该行为都是自动化为你完成的,所以它极大地简化了在应用程序中使用视图控制器的所需的工作。
关于创建故事板的完整细节在 Xcode User Guide 中有描述。目前,你需要知道一些在应用程序中实现故事板所需的必要术语。
一个场景表示一个屏幕上的内容区域,它由一个视图控制器管理。你可以把场景想象成一个视图控制器以及其相关的视图层次结构。
你在同一个故事板中创建场景之间的关系。关系在一个故事板中用从一个场景到另一个场景的一个连接箭头可视化表达。当你在两个对象之间做一个连接时,界面生成器通常自动推断一个新关系的细节。存在两种重要类型的关系:
包含表示两个场景之间的一个父子关系。当父控制器被实例化时,包含在其它视图控制器里的视图控制器也被实例化。比如,从一个导航控制器到另一个场景的第一个链接定义了把第一个视图控制器推送到导航栈。该控制器在导航控制器被实例化时自动被实例化。
在一个故事板中使用包含关系的一个好处是Interface Builder可以调整子视图控制器的外形来反映其祖先的存在。当内容视图控制器出现在你的最终应用程序中时,这样做允许 Interface Builder 显示内容视图控制器。
segue表示从一个场景到另一个场景的一个可视化过渡。在运行时,segues可以由不同的动作触发。 当一个segue被触发时,它导致一个新视图控制器被实例化并过渡到屏幕上。
尽管一个segue总是从一个视图控制器到另一个,有时该过程中可以涉及第三个对象。该对象实际触发segue。 比如,如果你做一个链接从一个按钮(该按钮在源视图控制器的视图层次结构中)到目标视图控制器,当用户点击该按钮,segue被触发。当直接从源视图控制器到目标视图控制器做一个segue,它通常表示你打算通过程序触发一个segue。
不同类型的segues提供了两个不同视图控制器之间所需的常用的过渡:
push segue 把目标视图控制器推送到一个导航控制器的栈中。
modal segue 直接显示目标视图控制器。
popover segue 在一个popover里显示目标视图控制器。
custom segue 允许你设计你自己的过渡来显示目标视图控制器。
Segues 共享一个通用编程模型。 在该模型中,目标控制器由iOS自动实例化,然后调用源视图控制器来配置它。 该行为符合控制流模型,该模型在 “Control Flow Represents Overall Coordination Between Content Controllers.” 中描述。
你还可以在一个视图控制器和存储在同一个场景中的对象之间创建链接。这些输出口(outlets) 和 动作(actions) 让你能仔细的定义视图控制器和其相关对象之间的关系。 关系(connections)本身在故事板中不是默认可见的,但是它们能在Interface Builder中的Connections Inspector中查看。
不管你是使用iOS提供的视图控制器,或是使用你创建的用来显示应用程序内容的自定义控制器,使用视图控制器的技术都是相似的。
使用视图控制器最常用的技术是把它们放入一个故事板中。把视图控制器放在故事板中让你直接建立视图控制器之间的关系,而不需要编写任何代码。 当应用程序第一次启动时,你可以查看控制流---从创建的控制器开始,到被实例化以响应用户的动作的控制器。iOS通过在应用程序需要时实例化这些视图控制器来管理大部分该过程。
有时你可能需要通过程序进行分配和初始化来创建一个视图控制器。 当你直接使用视图控制器时,你必须编写实例化视图控制器的代码,配置它并显示它。
图2-1 显示了一个故事板例子。 该故事板包含了视图控制器, 相关视图,以及在视图控制器之间建立关系的连接箭头。实际上,该故事板讲述了一个故事,以一个场景开始,然后显示其它场景以响应用户动作。
一个故事板可能指定一个视图控制器为初始视图控制器。如果一个故事板代表一个贯穿你的用户界面部分的指定工作流,初始视图控制器表示在那个工作流中的第一个场景。
你在故事板中为从初始视图控制器到其它视图控制器之间建立关系。然后,你建立从那些视图控制器到其它视图控制器之间的关系,最终连接大多数或全部故事板中的场景,组成一个唯一的连接图。 你建立的关系类型决定了何时实例化被连接的视图控制器,请看:
如果是一个segue关系,目标视图控制器在segue被触发时实例化。
如果是包含关系,子视图控制器在其父视图控制器被实例化时一起被实例化。
如果控制器不是目标或另一个控制器的子控制器,它绝不会自动被实例化。你必须通过程序从故事板把它实例化。
要想识别一个特定的视图控制器或一个故事板中的segue, 使用界面生成器给它分配一个标识字符串(identifier string)用来唯一识别它。要想通过程序从故事板加载一个视图控制器,你必须给它分配一个标识符。 相似地,要想通过程序触发一个segue,它也必须被分配一个标识符。当一个segue被触发时,那个segue的标识符被传递给源视图控制器,它被用来 决定该触发哪个segue。 因此,考虑给每个segue分配一个标识符。
当你使用故事板建立一个应用程序时,你可以使用一个单一故事板来装载它所有的视图控制器,或者你可以创建多个故事板并在每个故事板里实现用户界面的一部 分。应用程序中一个故事板几乎总是被指定为主故事板。如果有一个主故事板,iOS自动加载它;其它故事板必须被应用程序明确地加载。
主故事板被定义在应用程序的信息属性列表文件中。如果一个主故事板在该文件中被声明,然后当你的应用程序启动后,iOS执行以下步骤:
它为你实例化一个窗口。
它加载主故事板并实例化其初始视图控制器。
它把新的视图控制器分配给窗口的 rootViewController 属性,然后让窗口在屏幕上可见。
在初始视图控制器显示之前,它调用应用程序委托来对它进行配置。iOS加载主故事板的明确步骤在 “Coordinating Efforts Between View Controllers.” 中描述。
一个segue表示一个触发过渡,它把一个新视图控制器带入你的应用程序用户界面。
Segues包含很多关于过渡的信息,包括:
触发segue的对象, 它称为sender
开启segue的源视图控制器
要被实例化的目标视图控制器
过渡的类型,它用来把目标视图控制器带入屏幕。
一个可选的标识符字符串,它用来在故事板中识别特定的segue。
当一个segue被触发,iOS采取了以下动作:
它使用你在故事板中提供的属性来实例化目标视图控制器。
它给源视图控制器一个机会来配置新控制器。
它执行配置在segue中的过渡。
注意:当你实现自定义视图控制器时,每个目标视图控制器声明源视图控制器使用的公共属性和方法,以用来配置其行为。相应地,你的自定义源视图控制器重载由基类提供的故事板方法来配置目标视图控制器。具体详情,参见 “Coordinating Efforts Between View Controllers.”
一个segue通常因为跟源视图控制器相关的对象而被触发,一个控件或手势辨认者。然而,一个segue还可以通过程序在应用程序中被触发,只要它有一个分配的标识符。 比如,如果你正在实现一个游戏,你可能在比赛结束时触发一个segue。然后目标视图控制器显示比赛的最终分数。
你可以通过调用源视图控制器的 performSegueWithIdentifier:sender: 方法来程序性触发segue,给要被触发的segue传入标识符。你还需要传入另一个对象作为sender。当源控制器被调用来配置目标视图控制器时,segue的sender对象和标识符都提供给源控制器。
代码2-1 显示了一个触发一个segue的简单方法。 该例子是 “Creating an Alternate Landscape Interface.” 的 一部分。在这一小段中,你可以看到视图控制器正在接收一个方向(orientation)通知。当视图控制器处于竖直模式时,设备被转向水平方向,该方法使用一个segue来把一个不同的视图控制器呈现到屏幕上。因为本例中的通知对象不给执行该segue命令提供有用的信息,所以视图控制器让自己成为了sender.
1 - (void)orientationChanged:(NSNotification *)notification 2 { 3 UIDeviceOrientation deviceOrientation = [UIDevice currentDevice].orientation; 4 if (UIDeviceOrientationIsLandscape(deviceOrientation) && !isShowingLandscapeView) 5 { 6 [self performSegueWithIdentifier:@"DisplayAlternateView" sender:self]; 7 isShowingLandscapeView = YES; 8 } 9 }
如果一个segue只可以通过程序触发,你通常直接从源视图控制器画连接箭头到目标视图控制器。
你可能想要通过程序实例化一个视图控制器而不使用一个segue。故事板仍然很有价值,因为你可以使用它来配置视图控制器的属性以及它的视图层次结构。 然而,如果你确实通过程序实例化一个视图控制器,你得不到任何关于一个segue的行为。 要想显示视图控制器,你必须实现额外的代码。因为这个原因,在可能的情况下你应该依赖segues并在需要的时候才使用该技术。
以下是你的代码需要实现的步骤:
获取一个故事板对象( UIStoryboard 类的一个对象)
如果你有一个已经存在的视图控制器,它从同一个故事板中实例化,读取它的 storyboard 属性来取回故事板。 要想加载一个不同的故事板,调用 UIStoryboard
类的 storyboardWithName:bundle:类方法,传入故事板文件的名称和一个可选的束参数。
调用故事板对象的 instantiateViewControllerWithIdentifier:方法,传入你在界面生成器中创建视图控制器时你给它的标识符。
或者,你可以使用 instantiateInitialViewController方法来在实例化在故事板中的初始视图控制器,不需要知道它的标识符。
设置新视图控制器的属性来配置它。
显示新的视图控制器。参见 “Displaying a View Controller’s Contents Programmatically.”
代码2-2 显示了该技术的一个例子。 它从一个已经存在的视图控制器取回故事板并用它实例化一个新的视图控制器。
1 - (IBAction)presentSpecialViewController:(id)sender 2 { 3 UIStoryboard *storyboard = self.storyboard; 4 SpecialViewController *svc = [storyboard instantiateViewControllerWithIdentifier:@"SpecialViewController"]; 5 //Configure the new view controller here. 6 [self presentViewController:svc animated:YES completion:nil]; 7 }
代码2-3 显示了另一个频繁使用的技术。该例子加载了一个新故事板并实例化它的初始视图控制器。它使用该视图控制器作为一个新窗口的根视图控制器,该窗口将被放置在一个外接屏幕上。要想显示返回的窗口,你的应用程序可以调用窗口的 makeKeyAndVisible方法。
1 - (UIWindow *) windowFromStoryboard:(NSString *)storyboardName onScreen:(UIScreen *) screen 2 { 3 UIWindow *window = [[UIWindow alloc] initWithFrame:[screen bounds]]; 4 window.screen = screen; 5 6 UIStoryboard *storyboard = [UIStoryboard storyboardWithName:storyboardName bundle:nil]; 7 MainViewController *mainViewController = [storyboard instantiateInitialViewController]; 8 window.rootViewController = mianViewController; 9 //Configure the new view controller here. 10 return window; 11 }
Segues 只连接那些存储在同一个故事板中的场景。 要想显示一个从另一个故事板来的视图控制器,你必须明确地加载故事板文件并在里面实例化一个视图控制器。
没有要求你在应用程序里必须创建多个故事板。但是,在一些情况下,创建多个故事板可能对你有帮助:
你有一个庞大的编程团队,团队的不同部分被分配有不同用户界面部分。在这种情况下,每个子团队拥有一个故事板限制团队成员中的某人操作任何特定故事板中内容。
你已经获取或创建了一个库,该库预定义了一个视图控制器类型的集合;那些视图控制器的内容被定义在一个由库提供的一个故事板中。
你有需要被显示在一个外接屏幕的内容。在这种情况下,你可能保持所有的与备用屏幕(alternate screen)相关的视图控制器都在一个独立的故事板中。 相同情况下的一种替换方法是编写一个自定义segue。
当一个故事板中的容器被实例化时,它的子视图控制器同时自动被实例化。子视图控制器必须在同时被实例化,以供给容器控制器一些内容显示。
类似地,如果被实例化的子视图控制器还是一个容器,其子视图控制器也被同时实例化,一直到没有跟多的包含关系能被追踪到新的视图控制器。如果你把一个内容控制器放置在一个导航控制器,然后把该导航控制器放入一个标签栏控制器,当标签栏被实例化时,3个控制器同时被实例化。
容器以及其后裔在你调用视图控制器来配置它们之前被实例化。你的源视图控制器(或应用程序委托)可以依赖所有被实例化的子视图控制器。该实例化行为是重要的,因为你的自定义配置代码很少配置容器。相反,它配置连接到容器的内容控制器。
要想通过程序创建一个视图控制器,而不使用故事板,你需要使用Objective-C代码类分配和初始化视图控制器。你不能从故事板获取任何好处,意味着你必须实现额外的代码来配置并显示新视图控制器。
要想让一个视图控制器的内容有用,它需要被显示到屏幕上。这里有一些显示一个视图控制器内容的方法:
让视图控制器称为窗口的根视图控制器。
让它成为一个可见容器视图控制器的一个子视图控制器。
从另一个可见视图控制器呈现它。
使用一个popover(仅限 iPad)来呈现它。
在所有的情况当中,你把视图控制器分配给另一个对象---在这里是一个窗口,一个视图控制器,或一个弹出菜单控制器。该对象重新调整视图控制器的视图,并把它添加到自己的视图层次结构中然后显示它。
代码 2-4 显示了最常用的情况,它把视图控制器分配给一个窗口。该代码假设不使用故事板,因此它跟操作系统替你完成时执行同样的步骤:它创建一个窗口,设置新控制器为根视图控制器。然后让它显示。
1 - (void)applicationDidFinishLaunching:(UIApplication *)application 2 { 3 UIWindow *window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; 4 levelViewController = [[LevelViewController alloc] init]; 5 window.rootViewController = levelViewController; 6 [window makeKeyAndVisible]; 7 }
重要提示:绝不能直接把视图控制器的视图装入一个视图层次结构。为了正确呈现并管理视图,系统记录你显示的每个视图(以及它的相关视图控制器)。它在稍后使用该信息来给应用程序报告视图控制器相关的事件。比如,当设备的方向发生改变时,一个窗口使用该信息来认证最前面的视图控制器并通知其该变化。如果你通过其它方式把一个视图控制器的视图并入你的层次结构,系统可能不能正确地处理这些事件。
如果你正在实现你自己的自定义容器控制器,你把另一个视图控制器的视图添加到你自己的视图层次中,但是你还首先创建了一个父子关系。这样就能确保事件被正确地传递。参见 “Creating Custom Container View Controllers.”
自定义内容视图控制器是应用程序的心脏。你使用它们来呈现应用程序的独特内容。所有的应用程序至少需要一个自定义内容视图控制器。复杂的应用程序把工作分成多个内容控制器。
视图控制器有很多职责。 其中一些是iOS要求视图控制器来完成的。其它职责是当你在应用程序中定义它的角色时你需要分配给视图控制器的。
UIViewController 类提供了实现所有自定义视图控制器的基础结构。 你定义一个UIViewController类的子类。那个子类提供了必要的代码来用数据填充视图并响应用户动作。当你想对视图控制器的默认行为做调整时,你可以重载UIViewController类的方法。你的视图控制器还可能跟其它UIKit类交互来实现你想要的行为。
图3-1 显示了一些直接跟一个内容视图控制器相关的主要对象。这些对象都是视图控制器本身基本上拥有并管理的对象。视图(可通过 view 属性访问)是必须提供的唯一对象,尽管大多数视图控制器都有额外的子视图连接到该视图。一个内容视图控制器还包含需要显示的数据的自定义对象。
当你设置一个新的视图控制器,它可能有许多职责。其中一些职责面向内部,给视图以及视图控制器控制的其它对象使用。其它职责面向其它控制器。以下章节枚举了很多视图控制器的通用职责。
一些对象在视图控制器被初始化时实例化,在视图控制器被丢弃时释放。其它对象,比如视图,只在视图控制器的内容在屏幕上可见时才被需要。因此,视图控制器要有效地使用资源,并且应该准备好在内存不足时释放资源。在你的应用程序中正确地实现该行为,让你的应用程序更有效地使用内存和其它资源---比如CPU, GPU, 以及电池。
视图控制器管理它们的视图以及其子视图,但是视图的边框(frame)---它的位置和在其父视图中的尺寸---常常由其它因素决定,例如设备的方向,状态栏是否可见或甚至是视图控制器的视图如何在窗口里被显示等。你的视图控制器应该设计布局它的视图来适应提供给它的边框(frame)。
视图管理还有其它方面。当视图控制器的视图即将从屏幕显示或消失时,视图控制器会被通知。你的视图控制器可以使用该通知来执行对其操作来说必要的其它动作。
你的视图控制器常常是其视图和控件的中央协调对象。通常情况下,你设计的用户界面以便控件在用户操作它们时给控制器发送消息。你的视图控制器需要处理消息,对视图或存储在视图控制器中的数据做必要的改变。
视图控制器还参与用来给应用程序发送事件的响应链。你可以在视图控制器类里重载方法来让其直接参与事件处理。视图控制器还是实现其它行为的好对象---比如响应系统通知,定时器或应用程序特定的事件。
尽管一个视图控制器可能创建并管理很多其它对象,它通常并不需要公开暴露这些对象来检查或修改。它可能跟其它对象(特别是其它视图控制器)合作,但是它应该暴露最少数量的数学和必要方法来让它的合作者(collaborators)跟它交流。在视图控制器类中暴露太多的实现细节让修改视图控制器的实现变得困难。依赖这些实现细节的合作者可能需要被修改以继续跟你的视图控制器类一个工作。
如果你的视图控制器被放置在一个容器视图控制器中,容器施加(impose)了额外的限制,见图3-2. 容器可能要求你的视图控制器提供配置容器用户界面的其它对象。比如,一个放置在标签视图控制器内的内容视图控制器提供了一个标签栏来显示标签。
配置UIKit提供的容器的属性由 UIViewController 类定义。关于容器特定类型以及你可以配置的属性的更多信息,请看View Controller Catalog for iOS.
一些你设计的视图控制器打算让别的视图控制器来呈现。你可能直接呈现你的视图控制器,或者你可能让它称为一个容器视图控制器的子视图控制器,并呈现该容器。当它被呈现时,它显示在屏幕上直到它被丢弃。
以下是你可能呈现一个视图控制器的一些原因:
为了立即从用户那收集信息。
为了暂时呈现一些内容。
为了暂时改变工作模式。
为不同的设备方向实现替换界面。
为了呈现一个带有一种特定动画过渡类型(或没有过渡)的新视图层次结构。
大多数这些原因都设计短暂地打断应用程序的工作流以收集或显示一些信息。在几乎所有的情况当中,被呈现的视图控制器实现一个委托。被呈现的视图控制器使用 委托来跟呈现它的视图控制器交流。当你的应用程序拥有了它需要的信息之后(或者用户结束查看被呈现的信息),被呈现的视图控制器把这些信息传回给呈现它的 视图控制器。呈现它的视图控制器丢弃被呈现的视图控制器来让应用程序返回它的前一个状态。
在你的视图控制器中编写任何代码之前,你应该能够回答一些关于你打算怎么使用它的基本问题。以下提供的问题,旨在帮助你缩小你的视图控制器的焦点,并帮助 你理解它在应用程序中所扮演的角色。特别是,它帮助你识别你的视图控制器需要来执行它的各种任务的连接---通常是连到别的视图控制器的。
你是否使用了一个故事板来实现视图控制器?
它在什么时候被实例化?
它显示什么数据?
它需要执行什么任务?
它的视图是如何显示在屏幕上的?
它如何跟别的视图控制器发生合作?
如果你任然正在了解它所扮演的角色,你对这些问题的答案就不一定是精确的。 尽管如此,它有助于让你对视图控制器做什么以及其它对象如何与它发生交互有一个基本的概念。
以上问题不是要求你定义视图控制器的外形或者精确定义它如何执行你分配给它的任务的实现细节。那些都是你需要回答的重要问题,但是没有任何问题应该影响到 视图控制器的公共接口(public interface)。你想要灵活性来让视图控制器能够改变它的视觉设计,但是不改变类的声明,这些声明定义了其它控制器如何跟它协作。
你可能会考虑一个实现细节:是否使用一个故事板,但是你采用的方法会影响你如何实现视图控制器以及其它对象如何跟它协作。你应该总是使用一个故事板除非你有一个强烈原因不能这么做。
当你使用故事板时:
iOS通常自动为你初始化视图控制器。
要想结束实例化它,你可以重载它的 awakeFromNib 方法。
其它对象通过它的属性来配置它。
你创建它的视图层次结构和界面中的其它相关对象。这些对象在视图被需要时会自动被加载。
跟其它视图控制器之间的关系被创建在故事板中。
如果你通过程序设计你的视图控制器:
视图控制器通过分配和初始化来实例化。
你创建一个自定义初始化方法来初始化视图控制器。
其它对象使用视图控制器的的初始化方法以及通过配置它的属性来配置视图控制器。
你重载 loadView 方法来程序性创建和配置它的视图层次结构。
跟其它视图控制器之间的关系通过写代码来创建。
了解视图控制器何时被实例化通常意味着你的应用程序如何操作的其它细节。比如,你可能知道你的视图控制器总是由同一个对象实例化。通常实例化视图控制器的 对象本身就是视图控制器;这几乎总是在使用故事板的应用中出现的情况。 在任何时候,了解你的视图控制器何时,为什么,以及通过什么对象实例化等可以让你洞察视图控制器和创建它的对象之间的交换信息。
当你回答这两个问题时,你就是在理解应用程序的数据模型,以及是否有数据需要在视图控制器之间被交换。
以下是几种你应该能在你的视图控制器中遭遇的通用模式:
视图控制器从另一个控制器接收数据并显示它,不对数据进行编辑。没有返回数据。
视图控制器允许用户输入新数据。当用户完成数据编辑之后,它把新数据发送给另一个控制器。
视图控制器从另一个控制器接收数据,并允许用户对它进行编辑。当用户完成数据编辑之后,它把新数据发送给另一个控制器。
视图控制器不发送或接收数据。相反,它只显示静态视图。
视图控制器不发送或接收数据。作为替代,它的实现加载它的数据,但不给其它视图控制器暴露该机制。 比如, GKAchievementViewController 类有内建功能用以确定哪个玩家(player)在设备上被认证。 它还知道如何从游戏中心(Game Center)加载那个玩家的数据。呈现视图控制器(presenting view controller)不需要知道什么数据被加载或它是如何被加载。
你不仅限于使用这些设计。
当数据进入或离开你的视图控制器时,考虑定义一个数据模型类来装载准备进入到新控制器的数据。比如,在 Your Second iOS App: Storyboards 中, 主控制器使用一个BirdSighting 对象来发送跟详细控制器相关的数据。 使用一个这样的对象可以很容易就更新数据来包含附加的属性,而不需要改变控制器类中的方法签名。
一些视图控制器允许用户来查看,创建或编辑数据。其它视图控制器则允许用户来导航到其它内容屏幕。 甚至有一些控制器让用户来执行由视图控制器提供的任务。 比如 MFMailComposeViewController 类允许一个用户构建(compose) 和给其它用户发送邮件。它处理发送邮件消息的低层细节。
当你决定你的视图控制器执行哪个任务之后,决定你的视图控制器有多少控件(control)控制这些施加到其它控制器的任务。 很多视图控制器不需要给其它控制器施加配置数据就可以执行任务。比如, GKAchievementViewController 类给用户显示成就(achievements),它不需要任何属性来配置它如何加载或者呈现它的数据。MFMailComposeViewController 类则通过施加一些属性给另一个控制器让其可以配置它显示的初始内容,呈现一个稍有不同的方案。接着,用户可以编辑内容并发送email消息,不需要让其它控制器对象影响该过程。
一些视图控制器被设计成是根视图控制器。其它视图控制器则期望由另一个视图控制器呈现或放置在一个容器控制器中。偶尔,你会设计能以多种方式显示的控制 器。比如,一个分栏(split)视图控制器的主视图在横向模式中显示在分栏视图中,而在竖直模式中则显示在一个弹出控件中。
了解你的视图控制器如何被显示让你洞察它的视图是如何被定尺寸以及如何被放置到屏幕上。 它还影响别的区域,比如决定你的视图控制器跟其它什么控制器发生合作。
目前,你已经知道一些关于合作的事情。比如,如果你的视图控制器是从一个segue被实例化的,则它很可能跟配置它的源视图控制器发生合作。如果你的控制 器是一个容器的子视图控制器,则它跟容器发生合作。 但是也还有其它方向的关系。比如,你的视图控制器可能推迟一些它的工作,并把它传递给另一个视图控制器。它甚至可能跟一个已经存在的视图控制器交换数据。
使用这些所有的连接,你的视图控制器提供了一个界面供其它视图控制器使用,或者它意识到其它控制器并使用它们的界面。 为了提供一个无缝体验,这些连接是必不可少的,但是它们还代表了设计挑战,因为它们介绍应用程序中类之间的依赖关系。依赖关系是一个问题,因为它们让任何 一个类从组成应用程序的其它类中独立出来变得更加困难。因为这个原因,你需要平衡应用程序目前的需求和它的潜在需求来让应用程序为以后的改变保持足够的灵活性。
设计一个新视图控制器可以是具有挑战性的。 它有助于查看已经存在的设计以及理解它们多什么以及为什么。 以下章节讨论一些在iOS应用程序里通用的视图控制器风格。 每个实例都描述了视图控制器扮演的角色描述,简要说明它如何在高层工作,以及一个对以上问题所做的一个可能答案列表。
任务声明:允许用户在不同游戏风格间做选择的视图控制器。
描述:当一个游戏被启动时,它很少直接跳入真实游戏。相反,它显示一个标题屏幕,该屏幕标识游戏并给用户呈现一组游戏变量。 比如,一个游戏可能提供按钮,让玩家选择单一玩家或多个玩家模式。 当用户选择了其中一个选项,应用程序配置适当的配置本身并进入它的游戏界面。
标题屏幕很有趣,特别是因为它的内容都是静态的;它们不需要从另一个控制器获取数据。正因为如此,这个视图控制器几乎完全自给自足。然而,它确实知道其它视图控制器,因为它需要实例化其它视图控制器来启动它的玩法(gameplay)。
设计:
是否需要使用一个故事板来实现视图控制器?是的。
它什么时候被实例化?该视图控制器是主故事板中的初始场景。
它显示什么数据?该类显示预配置控件和图像;它不呈现用户数据。它不包含可配置的属性。
它执行什么任务?当用户点击一个按钮时,它触发一个segue来实例化另一个视图控制器。每个segue都已被认证以便能配置适当的游戏场景。
它的视图如何被显示到屏幕?它像根视图控制器一样被自动载入。
它如何跟别的视图控制器发生合作?它实例化另一个视图控制器来呈现一个游戏屏幕。当游戏结束时,其它视图控制器给标题(title)屏幕控制器发送一个消息来通知它游戏已经结束。标题(title)屏幕控制器然后斗气另一个视图控制器。
备选设计注意事项:
默认答案假设没有显示用户数据。某些游戏包含用户数据来配置视图或控件。比如:
你可能想视图控制器来显示用户的游戏中心别名。
你可能想让它根据设备是否跟游戏中心连接来启用或禁用按钮。
你可能想根据用户购买的应用程序内部采购项目来启用或禁用按钮。
当这些附加项被添加到设计,视图控制器需要一个更加传统的角色。它可能从应用程序委托接收数据对象或数据控制器以便它能根据需要查询并更新该状态。 或者,就像用户窗口的根视图控制器,你可能直接在标题(title)屏幕控制器中简单实现那些行为。真实设计很可能依赖于如何保证你的代码的灵活性。
任务声明:一个导航控制器的初始视图控制器,用来显示一系列应用程序的可用数据对象。
描述:在基于导航的应用程序中,主视图控制器是一个非常通用的部分。比如, Your Second iOS App: Storyboards 使用一个主视图来显示鸟类观察列表。当用户从列表中选择了一个sighting, 主视图控制器把一个新具体控制器推送到屏幕。
因为该视图控制器显示了数据项列表,它用 UITableViewController 子类代替 UIViewController
.
设计:
你是否需要使用一个故事板来实现视图控制器?是的。
它何时被实例化?跟一个导航控制器的根视图控制器一样,它跟它的父类同时被实例化。
它显示什么数据?一个应用程序数据的高层视图。它实现应用程序委托用来给它提供数据所用的各种属性。比如,bird watching 应用程序提供了一个自定义数据控制器对象给主视图控制器。
它执行什么任务?它实现一个添加(Add)按钮来让用户来创建新纪录。
它的视图如何被显示到屏幕上?它是一个导航控制器的子控制器。
它如何跟别的视图控制器发生合作?当用户点击列表中的一个项,它使用一个推送segue来显示一个细节控制器(detail controller). 当用户点击Add 按钮, 它使用一个模型segue来呈现一个新视图控制器,该新控制器用来编辑一个新纪录。 它接收从该模型视图控制器中返回的数据,并发送该数据给数据控制器来创建一个新的bird sighting.
备选设计注意事项:
当你构建一个iPhone应用程序时,使用一个导航控制器和一个初始视图控制器。 当你为iPad设计同样一个应用程序时,主视图控制器是一个分栏视图控制器(split view controller)的子控制器。其他大多数的设计决策保持不变。
任务声明:被推送到一个导航栈的控制器,用来显示被选中列表项的具体细节。列表项位于主视图控制器。
描述:详情视图控制器表示一个列表项的更加具体的视图,该列表项由主视图控制器显示。 当带有主视图控制器时,列表在一个导航栏界面显示。当用户结束查看数据项时,他们点击导航栏中的按钮返回到主视图。
Your Second iOS App: Storyboards 使用 UITableViewController 类来显示它的详情视图。它使用一个静态表格单元,每个单元可访问bird sighting数据的一个片段。 静态表格视图是实现该设计的一个好方法。
设计:
你是否使用一个故事板来实现视图控制器?是的。
它何时被实例化? 它通过一个推送segue从主视图控制器被实例化。
它显示什么数据?它显示存储在一个自定义数据对象中的数据。 它声明了由源视图控制器配置的各种属性来提供该数据。
它执行什么任务?它允许用户丢弃视图。
其视图如何被显示到屏幕上?它是导航控制器的一个子控制器。
它如何跟其它视图控制器发生合作?它从主视图控制器接收数据。
备选设计注意事项:当你构建一个iPhone应用程序时,最经常使用导航控制器。 当你为iPad设计同样一个应用程序时,主视图控制器是一个分栏视图控制器(split view controller)的子控制器。很多其它实现细节保持不变。
如果你的应用程序需要更多自定义视图行为,它可能子类化 UIViewController 类并实现它自己的自定义视图层次结构。
任务声明:允许用户撰写和发送邮件的视图控制器。
描述:Message UI框架提供了 MFMailComposeViewController 类。该类允许用户撰写和发送邮件。该视图控制器很有趣,因为它不仅仅能显示或编辑数据---它实际上能发送邮件。
该类的另一个有趣的设计选择是它它允许应用程序给邮件信息提供一个初始配置。 当初始配置已经被呈现后,用户可以在发送邮件之前重写这些选择。
设计:
你是否使用一个故事板来实现视图控制器?不。
它何时被实例化?它通过程序进行实例化。
它显示什么数据?它显示一个邮件信息的不同部分,包括收件人列表,标题,附件以及邮件信息本身。 该类提供允许另一个视图控制器预配置邮件信息的各种属性。
它执行什么任务?它发送邮件。
它的视图如何被显示到屏幕? 视图控制器由另一个视图控制器呈现。
它如何跟别的视图控制器发生合作?它返回状态信息给它的委托。 该状态允许呈现视图控制器(presenting view controller)了解一个邮件是否已经被发送。
对于你创建的任何自定义内容视图控制器,你必须让你的视图控制器处理以下一些任务:
你必须配置由视图控制器加载的视图。
自定义类可能需要加载特定方法来管理它的视图层次结构如何被加载或卸载。 这些同样的方法可能管理在同使创建的其它资源。参见 “Resource Management in View Controllers.”
你必须决定你的视图控制器支持哪些设备朝向以及当设备方向发生改变时它如何反应;参见 “Supporting Multiple Interface Orientations.”
当你实现你的视图控制器时,你很可能将发现你需要定义视图的动作方法或输出口(outlet)。比如,如果视图层次结构包含一个表格,你可能想在输出口里 存储一个指向该表格的指针,这样你就可以在稍候访问它。同样地,如果你的视图层次结构包含按钮或其它控件,你很可能想让那些控件调用视图控制器上的相关动 作方法。 当你遍历视图控制器类中的定义时,你可能因此发现你需要添加以下items到你的视图控制器类:
各种声明属性,它们指向各个对象,这些对象包含了由相关视图显示的数据。
公共方法和属性,它们向其它视图控制器揭露视图控制器的自定义行为。
输出口,它们指向视图层次结构中的各个视图,视图控制器必须跟这些输出口发生交互。
动作方法,它们执行跟在视图层次结构中的按钮和其它控件相关的任务。
重要提示:视图控制器的客户端不需要知道视图控制器显示什么视图或者那些视图可能触发哪些动作。 任何时候只要可能,输出口和动作都应该被声明在类的实现文件中的一个类别(category)中。 比如,如果你的类叫MyViewController,你添加以下声明到MyViewController.m中来实现类别:
1 @interface MyViewController() 2 //Outlets and actions here. 3 @end 4 @implementation MyViewController 5 //Implementation of the privately declared category must go here. 6 @end
当你声明一个没有名称的类别,属性和动作方法必须像在公共接口里的任何方法和属性一样在同一个实现块里实现。在似有类别里定义的输出口和动作方法在界面生 成器(Interface Builder)里可见,但是对于应用程序中的其它类来说不可见。 该策略让你不需要暴露类的秘密就可以获取界面生成器的好处。
如果另一个类需要访问 视图控制器的功能,添加公共方法和属性来访问该功能。
视图控制器是管理应用程序资源的一个必不可少的部分。视图控制器让你把应用程序分成多个部 分并只需要实例化需要的部分。但是不仅如此,一个视图控制器本身管理着不同的资源,并在不同时候实例化它们。比如,一个视图控制器的视图层次结构只在视图 被访问时才被实例化;通常,这只在视图被显示到屏幕上时才发生。 如果多个视图控制器被同时推送到一个导航栈,只有最顶端的视图控制器的内容是可见的,就是说只有该视图控制器的视图是可以被访问的。 类似地,如果一个视图控制器不是由一个导航控制器呈现,它不需要实例化它的导航项。通过推迟大多数资源分配直到它被需要,视图控制器使用更少的资源。
当内存允许应用程序以低内存运行时,所有的视图控制器都会自动接收到系统的提醒。 这样视图控制器就可以清楚哪些当内存富余时能很容易被重新创建的缓存和其它对象。确切的行为会根据应用程序运行的iOS版本不同而不同,而这些对你的视图 控制器设计都有影响(implications).
仔细管理跟视图控制器有关的各种资源是让应用程序有效运行的关键。 你应该还喜欢懒惰分配(lazy allocation); 哪些创建或者保持昂贵的各种对象应该在以后并且只在需要时才被分配。 因此,你的视图控制器应该让那些整个视图控制器生存期(lifetime)都需要的对象和只在必要时才需要的对象相分离。 当你的视图控制器接收到一个低内存警告,它应该在它不可见时随时准备缩减它的内存使用。
当一个视图控制器被第一次实例化时,它在生存期内创建或加载它需要的各种对象。 它不应该创建它的视图层次或跟显示内容相关的各种对象。 它应该聚焦于数据对象以及需要用来实现其关键行为的各种对象。
当你在故事板中创建了一个视图控制器,你在界面生成器(Interface Builder)里设置各种属性被序列化到一个压缩包(archive). 稍候,当视图控制器被初始化后,该压缩包被加载到内存并运行。结果是一个对象集,其属性符合你在界面生成器里设置的属性。 压缩包通过调用视图控制器的 initWithCoder: 方法来加载。 然后,在任何实现该方法的对象上调用 awakeFromNib 方法。 你可以使用该方法来执行任何配置步骤,这些步骤要求其它对象已经被初始化。
更多关于压缩和解压缩内容,请看 Archives and Serializations Programming Guide .
如果视图控制器通过程序分配其各种资源,创建一个专门用于该视图控制器的自定义初始化方法。该方法应该调用超类的 init 方法,然后执行任何类的特定初始化。
一般情况下,别编写复杂的初始化方法。相反,实现一个简单的初始化方法,然后给视图控制器的客户端(clients)提供各种特性(properties)来配置其行为。
任何时候,当应用程序中一个部分向视图控制器请求它的视图对象,但那个对象并不在当前的内存中时,视图控制器把视图层次加载进内存并把它存入其 view 特性以供将来引用。 加载过程中发生的步骤如下:
视图控制器调用其 loadView 方法。 loadView方法的默认实现完成以下事情之一:
如果视图控制器跟故事板相关联,它从故事板加载全部视图。
如果视图控制器跟故事板没有关联,创建一个空 UIView 对象并绑定到view 特性上。
视图控制器器调用其 viewDidLoad 方法,它让你的子类可以执行任何额外的加载期间任务。
图4-1 显示了加载过程的一个流程图,它包含了一些被调用的方法。 你的应用程序可以根据需要同时重写(override)loadView 和 viewDidLoad方法来实现(facilitate)你的视图控制器需要的各种行为。比如,如果应用程序没有使用故事板,但是你想要把额外的视图加 入视图层次,你可以通过程序重写loadView方法来实例化这些视图。
大多数视图控制器从一个相关联的故事板载入视图。使用故事板的优势是可以可视的布局和配置视图,让布局变得更加简单和快捷。 你可以快速地遍历(iterate)用户界面的不同版本,以一个优雅精美的设计而结束。
界面生成器是Xcode的一部分,它提供了一种直观的(intuitive)方法来创建和 配置视图控制器的视图。使用界面生成器,你集合(assemble)所有视图,并可以直接操纵控制它们,拖拉它们进入工作区,定位它们,重新定义它们的尺 寸以及使用一个属性(inspector)窗口来修改它们的属性。结果稍候被保存到一个故事板文件中,该文件存储了你集合的所有对象集,以及所有你做的自 定义信息。
为了让你能够正确地布局视图的内容,界面生成器提供了各种控件来让你指定视图是否有一个导 航栏,工具栏或其它可能影响你自定义内容位置的各种对象。 如果控制器被链接到故事板中的容器控制器上,它可以从容器控制器推断(infer)这些设置,使其更容易看到它究竟应该如何在运行时出现。
使用界面生成器,你可以在界面的视图和视图控制器之间创建各种链接。
代码4-1 显示了一个自定义MyViewController类的两个自定义输出口(用关键字 IBOutlet表明
)的声明,以及一个单个动作方法(由返回类型 IBAction 表示)。该声明在一个实现文件中的category中定义。 输出口把引用存入故事板中一个按钮和一个文本框中,动作方法响应按钮的点击。
代码 4-1 自定义视图控制器类的声明
1 @interface MyViewController() 2 @property (nonatomic) IBOutlet id myButton; 3 @property (nonatomic) IBOutlet id myTextField; 4 5 - (IBAction)myAction:(id)sender; 6 @end
图 4-2 显示了你在MyViewController类中各个对象之间的你将要创建的各种链接
图 4-2 故事板中的各个链接
当先前配置的MyViewController类被创建和呈现后,视图控制器基础设施 (infrastructure)自动从故事板加载视图并重新配置所有的输出口和动作。 因此,当视图被呈现给用户时,视图控制器的输出口和动作都以及被设置完成并可以使用。 在运行时代码和设计时(design-time)资源文件之间建立桥梁是让故事板如此强大的手段之一。
如果你更喜欢通过程序创建视图,而不使用故事板,你可以继承视图控制器的 loadView 方法来实现。 实现该方法需要完成以下步骤:
创建一个根视图对象
根视图包含了所有跟视图控制器相关的各种其它视图。你通常需要为该视图定义frame使其符合应用窗口的尺寸,应用程序窗口本身应该充满整个屏幕。 然而,调整frame应该基于视图控制器是如何被显示的。 参见
“Resizing the View Controller’s Views.”
你可以使用一个通用的 UIView 对象, 你定义的自定义视图,或任何其它视图都可以缩放到充满屏幕。
创建各种额外的子视图并把它们添加到根视图。
每个视图,你都应该:
创建并初始化视图。
使用 addSubview: 方法来把它添加到父视图。
如果你使用自动布局,给每个你刚创建的视图分配足够的限制(constaints)来控制视图的位置和尺寸。 或者,实现 viewWillLayoutSubviews
和 viewDidLayoutSubviews 方法来调整子视图在视图层次中的frames. 参见“Resizing the View Controller’s Views.”
把根视图绑定到视图控制器的 view 属性。
代码4-2 显示了一个实现loadView方法的示例。 该方法在视图层次里创建了一对自定义视图,并把它们绑定(assign)到视图控制器上。
列表 4-2 通过程序创建视图
1 - (void)loadView 2 { 3 CGRect applicationFrame = [[UIScreen mainScreen] applicationFrame]; 4 UIView *contentView = [[UIView alloc] initWithFrame:applicationFrame]; 5 contentView.backgroundColor = [UIColor blackColor]; 6 self.view = contentView; 7 8 levelView = [[LevelView alloc] initWithFrame:applicationFrame viewController:self]; 9 [self.view addSubview:levelView]; 10 }
提示:当你继承loadView方法来创建视图时,你不应该调用super. 这样做会触发(initiates)默认视图加载行为,而这通常只会浪费CPU周期(cycles). 你定义的loadView方法应该实现所有的工作,包括为你的视图控制器创建一个根视图和各个子视图。 关于视图载入进程的更多信息,参见 “A View Controller Instantiates Its View Hierarchy When Its View is Accessed.”
当你遇到视图控制器和内存管理时,你需要考虑以下两点:
如何有效地分配内存
何时以及如何释放内存
尽管内存分配的一些方面是需要你严格决定,但是 UIViewController 类提供了一些方法,它们通常跟内存管理任务有一些关联。 表格4-1 列出了一些在视图控制器对象中,你可能需要分配和回收内存的地方,以及一些你在每个地方应该做什么的信息。
表格 4-1 分配和回收内存的各个地方
Task | Methods | Discussion |
---|---|---|
Allocating critical data structures required by your view controller 分配视图控制器需要的各个关键数据结构 | Initialization methods 各种初始化方法 | Your custom initialization method (whether it is named 自定义初始化方法(方法名叫init 或其它都可以)经常负责把视图控制器对象放入一个已知的好状态。 该状态包含分配任何需要确保正确操作的数据结构。 |
Creating your view objects 创建视图对象 | <a href="https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewController_Class/Reference/Reference.html#//apple_ref/occ/instm/UIViewController/loadView" target="_self">loadView</a> | Overriding the 继承loadView方法只在你打算通过程序创建你的视图时需要。 如果你使用故事板,视图将自动从故事板文件上加载。 |
Creating custom objects 创建自定义对象 | Custom properties and methods 自定义属性和方法 | Although you are free to use other designs, consider using a pattern similar the 尽管你可以自由的使用其它设计,但是得考虑使用一个跟 loadView 方法相似的模式。 创建一个保存对象的属性以及一个相应的方法来初始化它。 当属性被读取而它的值为nil时,调用相关的加载方法。 |
Allocating or loading data to be displayed in your view 分配或加载显示到视图上的数据 | <a href="https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewController_Class/Reference/Reference.html#//apple_ref/occ/instm/UIViewController/viewDidLoad" target="_self">viewDidLoad</a> | Data objects are typically provided by configuring your view controller’s properties. Any additional data objects your view controller wants to create should be done by overriding the 数据对象通常通过配置视图控制器的各种属性来提供。 视图控制器想要创建的任何额外的数据对象都应该通过继承viewDidLoad 方法来实现。 当该方法被调用时,你的视图对象都保证存在并在一个已知的良好状态。 |
Responding to low-memory notifications 响应低内存通知 | <a href="https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewController_Class/Reference/Reference.html#//apple_ref/occ/instm/UIViewController/didReceiveMemoryWarning" target="_self">didReceiveMemoryWarning</a> | Use this method to deallocate all noncritical objects associated with your view controller. On iOS 6, you can also use this method to release references to view objects. 使用该方法来回收跟你的视图控制器关联的不太关键的对象的内存。在iOS6中,你还可以使用该方法来释放对视图对象的各个引用。 |
Releasing critical data structures required by your view controller 释放视图控制器要求的关键数据结构 | <a href="https://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/Classes/NSObject_Class/Reference/Reference.html#//apple_ref/occ/instm/NSObject/dealloc" target="_self">dealloc</a> | Override this method only to perform any last-minute cleanup of your view controller class. Objects stored in instance variables and properties are automatically released; you do not need to release them explicitly. 只在执行视图控制器类的最后一分钟清理时重写(override)该方法。存储在事例变量和属性中的对象都会自动被释放;你不需要明确地释放它们。 |
视图控制器的默认行为是当其 view 特 性被第一次访问时,加载其视图层次,然后一直把它保存在内存直到视图控制器被销毁。 视图用来在屏幕上绘制自身所需的内存有可能会比较大。 但是系统会在视图不显示在窗口时自动释放这些昂贵的资源。 保留下来供大多数视图使用的内存已经足够小,而不值得系统自动清除它们并重新创建视图层次。
你可以明确地释放视图层次,如果那个额外内存对于应用程序很必要。 列表4-3 重写了 didReceiveMemoryWarning 方 法来完成(accomplish)它。 首先,是调用超类的实现来获取任何需要的默认行为。 然后,清除视图控制器的资源。最后,测试视图控制器的视图是否在屏幕上可见。 如果视图跟窗口相关联, 则清除任何跟视图及其子视图之间的强引用。 如果视图存储了需要被重新创建的数据, 该方法的实现应该在释放任何跟那些视图之间的引用之前保存数据。
代码4-3 释放视图控制器中在屏幕上不可见的视图
1 - (void)didReceiveMemoryWarning 2 { 3 [super didReceiveMemoryWarning]; 4 //Add code to clean up any of your own resources that are no longer necessary. 5 if ([self.view window] == nil) 6 { 7 // Add code to preserve data stored in the views that might be 8 // needed later. 9 // Add code to clean up other strong references to the view in 10 // the view hierarchy. 11 self.view = nil; 12 } 13 }
当 view 特性第二次被访问时,会重新加载第一次访问时的视图。
当一个视图控制器的视图可见性发生改变时,视图控制器调用一些内建的方法来通知子类发生的 改变。 你可以重写(override)这些方法来实现(override)你的子类如何对改变做出反应。 比如,你可以使用这些通知来改变状态栏的颜色和方向,使其跟即将显示的视图风格相配套。
图5-1 显示了当一个视图控制器中的视图被添加到一个窗口视图层次里发生的一些列事件。 viewWillAppear: 方法 和 viewDidAppear: 方法让子类执行跟视图显示(appearance)相关的任何附加操作。
图 5-1 对一个视图的显示(appearance)做出响应
图5-2 显示了当一个视图从窗口移除时所发生的一系列事件。 当视图控制器检测到其视图即将被删除或隐藏时,它调用 viewWillDisappear: 和 viewDidDisappear: 方法来让子类执行任何相关任务。
图 5-2 响应一个视图的消失
有时候,知道一个视图出现或消失的原因很有用。 比如,你可能因为一个视图e刚刚被添加到一个容而器想要知道它是否被显示,或者因为遮盖(obscured)这个视图的一些内容被移除了而想知道该视图是 否被显示。 该特殊例子经常在使用导航控制器时出现;你的内容控制器中的视图可能在视图控制器刚被压入导航栈时出现,或者它可能因为前一个控制器被弹出栈而出现。
UIViewController 类提供了一些方法可以让视图控制器调用来确定显示状态(appearance)发生改变的原因。 表格5-1 描述了这些方法以及它们的用途。 这些方法能从 viewWillAppear: 和 viewDidAppear: 方法内部调用
Method Name | Usage |
---|---|
<a href="https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewController_Class/Reference/Reference.html#//apple_ref/occ/instm/UIViewController/isMovingFromParentViewController" target="_self">isMovingFromParentViewController</a> | You call this method inside your 你可以在 viewWillAppear: 和 viewDidAppear: 方法内部调用该方法,来确认视图控制器的视图是否正被隐藏,原因是视图控制器被从其容器视图控制器中删除。 |
<a href="https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewController_Class/Reference/Reference.html#//apple_ref/occ/instm/UIViewController/isMovingToParentViewController" target="_self">isMovingToParentViewController</a> | You call this method inside your 你可以在 viewWillAppear: 和 viewDidAppear: 方法内部调用该方法,来确定视图控制器的视图是否正被显示,因为视图控制器刚刚被加入到一个容器视图控制器。 |
<a href="https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewController_Class/Reference/Reference.html#//apple_ref/occ/instm/UIViewController/isBeingPresented" target="_self">isBeingPresented</a> | You call this method inside your 你可以在 viewWillAppear: 和 viewDidAppear: 方法内部调用该方法来确定视图控制器中的视图是否正被显示,因为视图控制器正被另一个视图控制器呈现(present). |
<a href="https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewController_Class/Reference/Reference.html#//apple_ref/occ/instm/UIViewController/isBeingDismissed" target="_self">isBeingDismissed</a> | You call this method inside your 你可以在 viewWillAppear: 和 viewDidAppear: 方法内部调用该方法来确定视图控制器的视图是否正被隐藏,因为视图控制器刚刚被释放(dismissed) |
视图控制器有自己的视图并管理视图的内容。 在过程中,视图控制器还管理视图的子视图。 但是在大多数情况下,视图的框架并不是由视图控制器直接设置。相反,视图的框架由视图控制器的视图如何显示决定。更直接地说,它由显示它的对象来配置。 应用程序中的其它情况,比如状态栏的出现也可以导致框架发生改变。 因此,当视图的框架变化时,你的视图控制器应该准备好调整它的内容。
跟窗口的根视图控制器相关的视图根据窗口的特性来获取框架。 由窗口设置的框架会基于以下因素发生改变:
wantsFullScreenLayout 特性值
如果你的应用程序显示状态栏,视图缩小以便它不与状态栏重叠(underlap)。 毕竟如果状态栏不透明,我们没有办法查看或跟状态栏下面的内容想交互。 然而,如果你的程序显示一个半透明状态栏,你可以把视图控制器的 wantsFullScreenLayout 特性设置为YES来让视图全屏显示。 状态栏覆盖在视图的顶部。
当你想要最大空间来显示你的内容时全屏很有用。 当在状态栏下面显示内容时,把那些内容放入一个滚动视图,这样用户滚动出在状态栏下面的内容。 让你的内容可以滚动是很重要的,因为用户不能跟位于状态栏或任何其它半透明的视图(比如半透明的导航栏和工具栏)后面的内容想交互。 导航栏自动添加一个滚动内容嵌入(inset to)到你的滚动视图(假设它是你的视图控制器的根视图),用来占用(account)导航栏的高度。另外,你必须手动修改滚动视图的 contentInset特性。
当一个视图控制器是一个容器视图控制器的子视图时,它的父视图决定哪些子视图可见。 当它想要显示视图时,它把它左右一个子视图添加到自己的视图层次结构并且设置它的框架让它适合用户界面。 比如:
一个子视图从其父视图那获取框架,一直延伸直到根视图控制器。根视图控制器的框架从窗口获取。
当一个视图控制器由另一个视图控制器呈现时,它的框架基于用来显示视图控制器的陈述上下文(presentation context)。 查看 “Presentation Contexts Provide the Area Covered by the Presented View Controller.”
由弹出控制器显示的视图控制器可以决定其视图区的尺寸,它把自己的 contentSizeForViewInPopover 特性值设置为需要的值。 如果弹出控制器把自己的 popoverContentSize 特性设置为一个不同的视图尺寸,它的尺寸值重写视图控制器的设置。 要想匹配其它视图控制器使用的模型,使用弹出控制器的特性来控制它的尺寸和位置。
当一个图控制器的尺寸改变时,它的子视图将被重新定位以适应为它们的新空间。 视图控制器的视图层次中的视图大多数由自己执行该工作,它们通过使用布局常量和自动尺寸调整蒙版(autoresizing masks). 然而,视图控制器也在不同点被调用,这样它就可以参与进程。以下是发生的事情:
视图控制器的尺寸被重新设置为新尺寸。
如果没有使用自动布局,视图根据它们的自动调整尺寸蒙版来调整尺寸。
调用视图控制器的 viewWillLayoutSubviews
调用视图的 layoutSubviews 方法。如果使用了自动布局来配置视图层次,它通过执行以下步骤来更新布局常量。
调用视图控制器的 updateViewConstraints 方法。
实现 UIViewController 类的 updateViewConstraints 方法调用视图的 updateConstraints方法。
布局常量更新以后,一个新布局被计算并且视图被重新定位。
调用视图控制器的 viewDidLayoutSubviews 方法。
理论上说,视图本身执行所有必要的工作来重新定位它们自己,而不需要视图控制器参与该过 程。 你常常可以在界面生成器里配置整个布局。然而,如果视图控制器动态地添加和删除视图,界面生成器中的一个静态布局不可能完成该工作。在这种情况下,视图控 制器是控制该过程的一个好地方,因为屏幕中视图本身常常只有其他视图的有限图片。 以下是在你的视图控制器上完成该工作的最好方法:
使用布局常量来自动定位视图(iOS 6 及以后版本). 重写 updateViewConstraints 方法来添加任何必要的视图没有配置的布局常量。该方法的实现必须调用 [super updateViewConstraints]
. 更多布局常量的信息,请看 Auto Layout Guide .
使用自动调整尺寸蒙版和代码的组合来手动定位视图(iOS 5.x). 重写 layoutSubviews 方法并使用它来定位任何视图,这些视图的位置不能通过使用重新调整尺寸蒙版来自动设置。 更多关于视图的自动调整尺寸特性以及它们如何影响视图的信息,请看 View Programming Guide for iOS .
视图控制器继承自 UIResponder 类, 因此它有能力处理所有事件。一般情况下,当一个视图不响应一个事件时,它一直把该事件向上传递给它的父视图,直到根视图。但是,当响应链中的视图是由一个 视图控制器管理时,它首先把不响应的事件传递给视图控制器对象,而不是它的父视图。这样视图控制器就可以响应并处理其视图不响应的各种事件。
图7-1 演示了一个视图层次中的事件流。 假设你又一个自定义视图,它被嵌入一个屏幕大小的普通视图对象中,而该普通视图由视图控制器管理。 传递到你的自定义视图的触摸事件被传递到普通视图进行处理。 如果你的视图不处理一个事件,则该事件被传递给它的父视图。 因为普通视图不处理事件,它把那些事件首先传递给它的视图控制器。 如果视图控制器不处理该事件,然后该事件进一步被传递给普通UIView对象的父视图,在本例中该父视图为window对象。
注意:一个视图控制器和它的视图之间的消息传递关系是由视图控制器私下管理的,它并不能被应用程序手动更改。
尽管你可能不想在你的视图控制器中处理触摸事件,但是你可以用它来处理基于运动的事件。你还可能用它来协调(coordinate)设置以及更改第一响应者。关于事件在iOS 应用程序中如何被发布(distrubuted)和处理的更多信息,请看 Event Handling Guide for iOS .