翻译自“View Controller Programming Guide for iOS”。
1 视图控制器的角色
视图控制器是应用程序内部结构的基础。每个应用至少有一个视图控制器,大多是应用程序有多个。每个视图控制器管理应用程序的部分用户界面,以及界面和底层数据之间的交互。视图控制器也便于用户界面不同部分之间的过渡。
因为视图控制器在应用程序中扮演了如此重要的角色,所以它们几乎是你做一切事情的中心。UIViewController类定义了管理视图,处理事件,从一个视图控制器过渡到另一个,与应用程序其它部分交互的方法和属性。可以从UIViewController(或它的子类)继承,并添加自定义代码来实现应用程序的行为。
有两种类型的视图控制器:
- 内容视图控制器管理独立的应用程序内容,你创建的大部分视图控制器都是这种类型。
- 容器视图控制器从其它视图控制器(称为子视图控制器)收集信息,并用一种不同的,方便导航或显示这些视图控制器内容的方式显示。
大部分应用程序混用两种类型的视图控制器。
1.1 视图管理
视图控制器最重要的作用是管理视图的层级结构。每个视图控制器都有唯一的根视图,该视图包含所有视图控制器的内容。添加需要显示的内容到根视图。图1-1描述了视图控制器和它的视图之间的内在关系。视图控制器总有一个指向根视图的引用,每个视图有一个指向它的子视图的强引用。
图1-1 视图控制器和它的视图之间的关系
提示:常见的做法是使用outlet访问视图控制器中视图层级结构的其它视图。因为视图控制器管理它所有子视图的内容,所以outlet可以存储指向需要的视图的引用。当从故事版中加载视图时,outlet本身自动连接到实际的视图对象。
内容视图控制器管理自身的所有视图。容器视图控制器除了管理自身的视图,还要管理一个或多个子视图控制器的根视图。容器不管理子视图控制器的内容。它只管理根视图,并根据容器的设计管理根视图的尺寸和位置。图1-2描述了分割视图控制器和它的子视图控制器之间的关系。分割视图控制器管理子视图的整体尺寸和位置,而子视图控制器管理这些视图的实际内容。
图1-2 视图控制器可以管理其它视图控制器的内容
如何管理视图控制器的视图,请参考“管理视图布局”。
1.2 数据打包
视图控制器是它管理的视图和应用程序数据之间的媒介。UIViewController类的方法和属性可以用来管理应用程序的视觉呈现。继承UIViewController类时,可以在子类中添加任意需要的变量来管理数据。添加自定义变量会创建类似图1-3中的关系,视图控制器有指向数据的引用和用来显示数据的视图的引用。你负责两者之间的数据通信。
图1-3 视图控制器是数据对象和视图之间的媒介
你应该始终保持视图控制器和数据对象之间职责分离。确保数据结构完整性的大部分逻辑属于数据对象本身。视图控制器可能会验证来自视图的输入,然后以数据对象要求的格式打包该输入,但应该最小化视图控制器在管理实际数据上的工作。
UIDocument对象是管理数据的一种方式,该方式可以保持视图控制器和数据对象之间职责分离。文档对象是知道如何读写数据到持久存储的控制器对象。继承UIDocument时,可以添加任何逻辑和方法来提取数据,并把它传递给视图控制器或应用程序其它部分。视图控制器可能会存储它收到的任何数据的副本,以便更新视图,但是文档对象仍然拥有真实的数据。
1.3 用户交互
视图控制器是响应者对象(responder object),能够处理响应链中的事件。尽管视图控制器可以这么做,但它们很少直接处理触摸事件。相反,通常视图处理自己的触摸事件,并把结果反馈给关联的代理或目标对象(通常是视图控制器)的方法。所以,使用代理方法或动作方法(action method)处理视图控制器中的大部分事件。
关于如何在视图控制器中实现动作方法,请参考“处理用户交互”。关于处理其它类型的事件,请参考“iOS事件处理指南”。
1.4 资源管理
视图控制器负责它的视图和它创建的任何对象。UIViewController类自动处理视图管理的大部分方面。例如,UIKit自动释放不要需要的视图相关资源。在UIViewController子类中,你负责管理显式创建的任何对象。
当可用内存不足时,UIKit要求应用释放所有不再需要的资源。其中一种方式是调用视图控制器的didReceiveMemoryWarning方法。使用该方法移除指向不再需要的对象的引用,或者稍后可以很容易重新创建的引用。例如,使用该方法移除缓存数据。当内存不足时,尽可能多的释放内存。消耗太多内存的应用可能会被系统完全终止,以便恢复内存。
1.5 自适应
视图控制器负责显示视图,和调整显示来匹配底层环境。每个iOS应用应该可以在iPad和不同尺寸的iPhone上运行。比起为每种设备提供不同的视图控制器和视图层级结构,使用单个视图控制器,并让它的视图适应空间的变化更简单。
在iOS中,视图控制器需要处理粗粒度和细粒度的变化。粗粒度变化发生在视图控制器的特征(trait)改变时。特征是描述整体环境的属性,比如显示比例。两个最重要的特征是视图控制器水平和垂直方向的尺寸类(size class),表示在给定的尺寸中,视图控制器有多少可用空间。可以使用尺寸类的变化来改变视图布局的方式,如图1-4所示。当水平方向的尺寸类是常规时,视图控制器利用额外的水平空间排列内容。当水平方向的尺寸类是紧凑时,视图控制器垂直排列内容。
图1-4 让视图适应尺寸类的变化
给定尺寸类后,在任何时候都有可能发生更细粒度的尺寸变化。当用户将iPhone从竖屏旋转到横屏时,尺寸类也许不会变化,但屏幕尺寸通常会变化。使用自动布局(Auto Layout)时,UIKit自动调整视图的尺寸和位置来匹配新的尺寸。视图控制器可以根据需要做额外的调整。
更多关于自适应的信息请参考“自适应模型”。
2 视图控制器层级结构
应用程序的视图控制器之间的关系定义了每个视图控制器所需要的行为。UIKit希望你按规定的方式使用视图控制器。保持适当的视图控制器关系可以保证,需要时那些自动行为可以传递给正确的视图控制器。如果违反了规定的包含和显示关系,应用程序的一部分不会按预期运行。
2.1 根视图控制器
根视图控制器是视图控制器层级结构中的支撑点。每个窗口都恰好有一个根视图控制器,其内容充满该窗口。根视图口控制器定义了用户看到的初始内容。图2-1展示了根视图控制器和窗口之间的关系。因为窗口本身没有可见内容,所以视图控制器的视图提供了所有内容。
图2-1 根视图控制器
根视图控制器通过UIWindow对象的rootViewController属性访问。使用故事版配置视图控制器时,UIKit在启动时自动设置该属性的值。对于通过代码创建的窗口,必须自己设置根视图控制器。
2.2 容器视图控制器
容器视图控制器可以把更好管理和重用的部件组合为复杂的界面。容器视图控制器混合一个或多个带有可选自定义视图的子视图控制器来创建最终的界面。例如,UINavigationController对象显示带有导航栏和可选工具栏的子视图控制器的内容,导航栏和可选的工具栏由导航控制器管理。UIKit包含几个容器视图控制器:UINavigationController,UISplitViewController和UIPageViewController。
容器视图控制器的视图总是充满给它的空间。容器视图控制器通常作为窗口的根视图控制器(如图2-2所示),但它们也可以模态的显示或者作为其它容器的子视图控制器。容器负责适当的定位它们的子视图。在图中,容器并排放置两个子视图。尽管子视图控制器依赖于容器界面,但它们几乎不知道容器和任何兄弟视图控制器。
图2-2 容器视图控制器作为根视图控制
因为容器视图控制器管理它的子视图控制器,所以UIKit定义了如何在自定义容器中子视图控制器的规则。如何创建自定义容器视图控制器的详细信息,请参考“实现容器视图控制器”。
2.3 Presented视图控制器
显示一个视图控制器会用新视图控制器的内容代替当前视图控制器的内容,通常是隐藏前一个视图控制器的内容。弹出(presentation)最常用来模态显示新的内容。例如,显示一个视图控制器用来收集用户的输入。也可以把它们作为应用程序界面的通用组成部分。
显示一个视图控制器时,UIKit在presenting视图控制器和presented视图控制器之间创建了如图2-3所示的关系。(也存在从presented视图控制器到presenting控制器的反向关系。)这些关系是视图控制器层级结构的一部分,同时也是运行时定位其它视图控制器的一种方式。
图2-3 presented视图控制器
涉及到容器视图控制器时,UIKit可能会修改显示链来简化必须编写的代码。不同的显示风格如何在屏幕上显示有不同的规则。例如,全屏显示总是覆盖整个屏幕。显示视图控制器时,UIKit寻找一个为显示提供合适上下文的视图控制器。很多情况下,UIKit选择最近的容器视图控制器,但也可能选择窗口的根视图控制器。某些情况下,也可以告知UIKit哪个视图控制器定义了显示上下文,应该处理该显示。
图2-4展示了为什么通常容器提供了显示上下文。执行全屏显示时,新的视图控制器需要覆盖整个屏幕。容器决定是否处理显示,而不是要求子视图控制器知道容器的bounds。因为例子中的导航控制器覆盖了整个屏幕,所以它作为presenting视图控制器,并初始化显示。
图2-4 容器和presented视图控制器
3 设计技巧
视图控制器是iOS上应用程序运行的重要工具,UIKit中的视图控制器基础架构可以不用编写大量的代码就能创建复杂的界面。实现自己的视图控制器时,使用下面的技巧和指南来确保不会干扰系统期望的行为。
3.1 尽可能使用系统提供的视图控制器
很多iOS框架定义了可以使用的视图控制器。使用系统提供的视图控制器可以节省时间,并确保用户体检的一致性。
绝大部分系统视图控制器是为特定任务设计的。一些视图控制器可以访问用户数据,例如联系人。其它视图控制器可以访问硬件,或者为管理多媒体提供特殊的调解界面。例如,UIKit中的UIImagePickerController类显示一个标准界面,用来捕获图片和视图,以及访问用户的相册。
创建自定义视图控制器之前,查看现在框架中是否存在满足需求的视图控制器。
- UIKit框架提供了显示警告框,拍摄图片和视频,管理iCloud文件的视图控制器。还定义了很多用来组织内容的标准容器视图控制器。
- GameKit框架提供了匹配玩家,管理排行榜,成就和其它游戏特性的视图控制器。
- Address Book UI框架提供了显示和选择联系人信息的视图控制器。
- MediaPlayer框架提供了显示和管理视图,以及从用户库中选择多媒体资源的视图控制器。
- EventKit UI框架提供了显示和编辑用户日历数据的视图控制器。
- GLKit框架提供了管理OpenGL渲染表面的视图控制器。
- Multipeer Connectivity框架提供了检测其它用户和邀请连接的视图控制器。
- Message UI框架提供了编写电子邮件和短信的视图控制器。
- PassKit框架提供了显示pass和添加pass到Passbook的视图控制器。
- Social框架提供了编写Twitter,Facebook和其它社交媒体网站消息的视图控制器。
- AVFoundation框架提供了显示多媒体资源的视图控制器。
重要:永远不要修改系统提供的视图控制器的视图层级结构。每个视图控制器有自己的视图层级结构,并负责维护该层级结构的完整性。改变层级结构可能会在代码中引入bug,或者让其中的视图控制器不能正常工作。在系统视图控制器中,总是使用公有方法和属性修改视图控制器。
关于使用特定视图控制器的信息,请参考相应的框架文档。
3.2 让每个视图控制器相互独立
视图控制器应该总是自我独立的对象。视图控制器不应该了解内部运作,或者其它视图控制器的视图层级结构。两个视图控制器需要通信或者相互传递数据时,应该使用显式定义的公开接口。
代理设计模式经常用来管理视图控制器之间的通信。使用代理时,一个对象定义一个协议,用来与关联的代理对象通信,该代理对象遵循这个协议。代理对象的确切类型不重要。重要的是该对象实现了协议的方法。
3.3 根视图只作为其它视图的容器
视图控制器的根视图仅仅作为内容的容器。使用根视图作为容器让所有视图有公有的父视图,可以让许多布局操作更简单。很多自动布局约束要求有公有的父视图来正确的布局视图。
3.4 知道你的数据在哪
在model-view-controller设计模式中,视图控制器的作用是方便数据在模型对象和视图对象之间传递。视图控制器可能在临时变量中存储一些数据,并执行一些验证,但它的主要职责是确保它的视图包括准确的信息。数据对象负责管理真正的数据,并确保属性的完整性。
UIDocument和UIViewController类之间的关系就是分割数据和界面的例子。具体的说,两者之间不存在默认关系。UIDocument对象协调加载和保存数据,而UIViewController对象协调在屏幕上显示视图。如果在两个对象之间创建了关系,记住视图控制器从文档中缓存信息只是为了效率。真正的数据仍然属于文档对象。
3.5 适应变化
应用程序可以在各种iOS设备上运行,视图控制器设计为可以适应这些设备的不同尺寸屏幕。使用内置的自适应支持来响应视图控制器中尺寸和尺寸类的变化,而不是使用不同的视图控制器来管理不同屏幕上的内容。UIKit发送的通知让你可以大规模和小规模改变用户界面,而不用改变视图控制器的其余代码。
关于处理自适应变化的更多信息,请参考“自适应模型”。