Introduction
关于窗口和视图(windows & views)
在 iOS 中,你使用窗口和视图在屏幕上呈现应用程序的内容。窗口本身没有可见内容,但为应用程序的视图提供了基本容器。视图定义了窗口中你想填充某些内容的部分。例如,你可能有显示图像、文本、形状或它们组合的视图。你也可以使用视图来组织和管理其他视图。
概述
每个应用程序至少有一个窗口和一个视图用于展示其内容。UIKit 和其他系统框架提供了预定义的视图,你可以用来展示你的内容。这些视图从简单的按钮和文本标签到更复杂的视图如表格视图、选择器视图和滚动视图不等。在预定义视图无法满足需求的地方,你也可以定义自定义视图并自行管理绘制和事件处理。
视图管理您的应用程序的视觉内容
视图是 UIView
类(或其子类)的一个实例,并管理应用程序窗口中的矩形区域。视图有三个任务
- 绘制内容,绘制涉及使用图形技术如 Core Graphics、OpenGL ES 或 UIKit,在视图的矩形区域内绘制形状、图像和文本。
- 处理多点触摸事件,视图通过手势识别器或直接处理触摸事件响应在其矩形区域内发生的触摸事件。
- 管理子视图的布局,在视图层次结构中,父视图负责定位和调整其子视图的大小,并且可以动态进行。这种动态修改子视图的能力使你的视图能够适应变化的条件,如界面旋转和动画。
你可以将视图视为构建用户界面的积木。而不是使用一个视图来展示所有内容,你通常会使用多个视图来构建视图层次结构。层次结构中的每个视图都展示用户界面的特定部分,并通常针对特定类型的内容进行了优化。例如,UIKit 提供了专门用于展示图像、文本及其他类型内容的视图。
窗口协调视图的显示
窗口是 UIWindow
类的一个实例,处理应用程序用户界面的整体展示。窗口与视图(及其所属的视图控制器)协作,管理和改变可见视图层次结构的交互。大多数情况下,你的应用程序窗口不会改变。一旦创建后,它保持不变,只有它所显示的视图会发生变化。每个应用程序至少有一个窗口,用于在设备的主要屏幕上显示应用程序的用户界面。如果设备连接了外部显示器,应用程序可以创建第二个窗口以在该屏幕上展示内容。
动画为界面变化提供用户可见反馈
动画为用户提供关于视图层次结构变化的可见反馈。系统定义了用于展示模态视图和不同视图组之间转换的标准动画。然而,许多视图属性也可以直接进行动画处理。例如,通过动画你可以改变视图的透明度、屏幕上的位置、大小、背景颜色或其他属性。如果你直接与视图底层的 Core Animation 层对象工作,还可以执行许多其他动画。
Interface Builder 的角色
Interface Builder 是一个用于图形化地构造和配置应用程序窗口和视图的应用程序。使用 Interface Builder,你可以组装视图并将它们放置在一个 nib 文件中,该文件是一个存储视图和其他对象的“冻结干燥”版本的资源文件。在运行时加载 nib 文件时,其中的对象会被重新构成成实际对象,然后可以通过代码进行编程操作。
Interface Builder 极大地简化了你在创建应用程序用户界面时的工作量。由于对 Interface Builder 和 nib 文件的支持贯穿整个 iOS,因此将 nib 文件纳入应用程序设计需要的努力非常少。
有关如何使用 Interface Builder 的更多信息,请参阅 Interface Builder User Guide。有关视图控制器如何管理包含其视图的 nib 文件的信息,请参阅 View Controller Programming Guide for iOS。
相关章节
因为视图是非常复杂和灵活的对象,本文档不可能涵盖它们的所有行为。不过,还有其他文档可以帮助您了解管理视图和整个用户界面的其他方面。
- 视图控制器是管理应用程序视图的重要组成部分。视图控制器监督单个视图层次结构中的所有视图,并促进这些视图在屏幕上的展示。有关视图控制器及其角色的更多信息,请参阅 View Controller Programming Guide for iOS。
- 视图是应用程序中手势和触摸事件的关键接收者。有关使用手势识别器和直接处理触摸事件的更多信息,请参阅 Event Handling Guide for iOS。
- 自定义视图必须使用可用的绘图技术来渲染其内容。有关在视图内使用这些技术进行绘制的信息,请参阅 Drawing and Printing Guide for iOS。
- 在标准视图动画不足的情况下,你可以使用 Core Animation。有关使用 Core Animation 实现动画的信息,请参阅 Core Animation Programming Guide。
View and Window Architecture
视图和窗口呈现应用程序的用户界面,并处理与该界面的交互。UIKit 和其他系统框架提供了许多可以直接使用、几乎无需修改的视图。你也可以在需要以不同于标准视图的方式展示内容时,定义自定义视图。
无论你使用系统提供的视图还是创建自己的自定义视图,都需要理解 UIView
和 UIWindow
类所提供的基础结构。这些类提供了管理视图布局和展示的复杂机制。了解这些机制的工作原理对于确保你的视图在应用程序发生变化时能正确响应至关重要。
视图架构基础
大多数你想通过视觉方式实现的功能都是通过视图对象完成的 —— 也就是 UIView
类的实例。一个视图对象定义了屏幕上的一个矩形区域,并负责在该区域内进行绘制和处理触摸事件。视图还可以作为其他视图的父容器,并协调这些子视图的位置和尺寸。UIView
类已经完成了大部分视图之间关系的管理工作,但你也可以根据需要自定义默认行为。
视图与 Core Animation 层协同工作,用于处理视图内容的渲染和动画效果。UIKit 中的每个视图都由一个层对象(通常是 CALayer
类的实例)支持,该对象管理视图的内容存储并处理与视图相关的动画。大多数操作应通过 UIView
接口进行。然而,在你需要对视图的渲染或动画行为进行更精细控制的情况下,可以通过其对应的层来执行操作。
为了理解视图和层之间的关系,我们来看一个示例。图 1-1 显示了来自 ViewTransitions 示例应用的视图架构及其与底层 Core Animation 层的关系。该应用中的视图包括一个窗口(它本身也是一个视图)、一个作为容器视图的通用 UIView
对象、一个图像视图、一个用于显示控件的工具栏以及一个工具栏按钮项(它本身不是一个视图,但内部管理一个视图)。(实际的 ViewTransitions 示例应用中还包含另一个用于实现过渡效果的图像视图。为简化说明且由于该视图通常被隐藏,图 1-1 中未将其包括在内。) 每个视图都有一个对应的层对象,可通过该视图的 layer
属性访问。(由于工具栏按钮项不是一个视图,因此不能直接访问其层。)这些层对象背后是 Core Animation 的渲染对象,最终则是用于管理屏幕上像素数据的硬件缓冲区。
图 1-1:示例应用中的视图架构
使用 Core Animation layer 对象对性能有重要影响。视图对象的实际绘图代码尽可能少地被调用,一旦被调用,其结果将由 Core Animation 缓存并在之后尽可能重复使用。这种已渲染内容的重用消除了通常更新视图所需的昂贵绘图周期。在动画过程中,这种内容的复用尤为重要,因为可以对现有内容进行操作。这种复用比创建新内容要高效得多。
视图层次结构与子视图管理
除了提供自己的内容外,视图还可以作为其他视图的容器。当一个视图包含另一个视图时,两个视图之间就建立了父子关系。在这种关系中,子视图被称为 Subview,而父视图则被称为 Superview。这种关系的建立不仅会影响应用程序的视觉外观,也会影响其行为。
从视觉上看,子视图的内容会遮挡其父视图的部分或全部内容。如果子视图完全不透明,则其所占区域会完全遮盖父视图的相应部分;如果子视图是半透明的,则两个视图的内容会在显示到屏幕之前混合在一起。每个父视图将其子视图存储在一个有序数组中,数组中的顺序也会影响各个子视图的可见性。如果有两个兄弟子视图发生重叠,后添加(或移动至子视图数组末尾)的那个会显示在另一个之上。
Superview-Subview 关系也会影响多个视图的行为。改变父视图的大小会产生连锁反应,可能导致其子视图的大小和位置也发生变化。当你改变父视图的大小时,可以通过适当地配置视图来控制每个子视图的调整行为。其他会影响子视图的变化包括隐藏父视图、更改父视图的 alpha 值(透明度),或对父视图的坐标系应用数学变换。
视图在视图层次结构中的排列方式也决定了你的应用程序如何响应事件。当某个特定视图内部发生触摸事件时,系统会将包含触摸信息的事件对象直接发送给该视图进行处理。但如果该视图没有处理这个触摸事件,它可以将事件对象传递给其父视图。如果父视图也没有处理该事件,它将继续向上传递给上一级父视图,依此类推,形成所谓的“响应链”。特定的视图也可以将事件对象传递给中间的响应者对象,例如视图控制器。如果没有对象处理该事件,它最终会到达应用程序对象,通常会被丢弃。
视图绘制周期
UIView
类使用一种按需绘制(on-demand drawing)的模型来呈现内容。当一个视图首次出现在屏幕上时,系统会请求它绘制其内容。系统捕获该内容的一个快照,并将这个快照作为视图的视觉表示。如果你从未更改视图的内容,那么该视图的绘制代码可能再也不会被调用。这个快照图像会被用于大多数与视图相关的操作中。如果你确实更改了内容,你需要通知系统视图已经发生变化。视图随后会重复「绘制内容并捕获新结果的快照」这一过程。
当你视图的内容发生改变时,你不应直接重绘这些变化。相反,你应该使用 setNeedsDisplay
或 setNeedsDisplayInRect:
方法使视图失效(invalidate)。这些方法告诉系统该视图的内容已改变,需要在下一个合适的机会重新绘制。系统会等到当前运行循环(run loop)结束之后才开始绘制操作。这种延迟机制为你提供了一次时机可以做以下事情:
- 使多个视图失效
- 添加或移除视图
- 隐藏、调整大小或重新定位视图
所有你做的更改将会同时反映出来。
注意: 更改视图的几何形状(frame)并不会自动导致系统重绘视图内容。视图的
contentMode
属性决定了如何解释对视图几何形状的更改。大多数 content mode 会在视图边界内拉伸或重新定位现有的快照,而不会创建新的快照。
当需要渲染视图内容时,实际的绘制过程会根据视图及其配置的不同而有所变化。系统视图通常实现私有的绘制方法来渲染其内容。这些系统视图往往还提供了接口供你配置视图的外观。对于自定义的 UIView
子类,你通常会重写 drawRect:
方法,并在该方法中绘制视图的内容。当然还有其他方式可以为视图提供内容,例如直接设置底层层对象的 contents
属性,但重写 drawRect:
是最常见的方式。
内容模式(Content Modes)
每个视图都有一个 contentMode
属性,它控制着当视图的几何形状发生变化时,视图如何(或者是否)复用其内容。当视图第一次显示时,它会像往常一样渲染内容,结果会被存储在一个底层的位图中。此后,对视图几何形状的更改并不总是导致位图被重新创建。相反,contentMode
属性的值决定了是将位图缩放以适应新的边界,还是简单地将其固定在视图的一个角落或边缘上。
以下情况会触发应用 contentMode
:
- 更改视图的
frame
或bounds
的宽度或高度 - 给视图的
transform
属性赋值一个包含缩放因子的变换矩阵
默认情况下,大多数视图的 contentMode
属性设置为 UIViewContentModeScaleToFill
,这会导致视图内容被缩放以适配新的 frame 尺寸。
图 1-2:不同 content mode 的对比效果
如图所示,并非所有 content mode 都能完全填满视图的边界,即使填充了,也可能造成内容变形。
虽然 content mode 很适合用来复用视图内容,但你也可以将 contentMode
设置为 UIViewContentModeRedraw
,以便在缩放和调整大小操作期间强制你的自定义视图进行重绘。将视图的 contentMode
设为此值会迫使系统在几何形状变化时调用视图的 drawRect:
方法。
一般来说,应尽可能避免使用这个值,尤其不要将其用于标准的系统视图。
有关可用 content mode 的更多信息,请参阅 UIView 类参考文档。
可拉伸视图
你可以指定视图的一部分为可拉伸的,这样当视图大小发生变化时,只有可拉伸部分的内容会受到影响。通常你会在按钮或其他具有重复图案的视图中使用可拉伸区域。你可以指定一个视图沿一个或两个轴进行拉伸。当然,如果要沿两个轴拉伸视图,视图的边缘也必须定义一个可重复的模式以避免任何失真。图 1-3 展示了在一个视图中这种失真的表现形式,即每个原始像素的颜色被复制填充到较大视图中的相应区域。
图 1-3:拉伸按钮的背景
你可以使用 contentStretch
属性来指定视图的可拉伸区域。该属性接受一个矩形,其值归一化到 0.0 到 1.0 的范围内。在拉伸视图时,系统将这些归一化的值乘以视图当前的边界和比例因子来确定需要拉伸哪些像素。使用归一化的值免去了每次视图边界变化时更新 contentStretch
属性的需求。
视图的 content mode 同样决定了如何使用视图的可拉伸区域。仅当 content mode 会导致视图内容被缩放时,才会使用可拉伸区域。这意味着仅在 UIViewContentModeScaleToFill
、UIViewContentModeScaleAspectFit
和 UIViewContentModeScaleAspectFill
这些 content modes 下支持可拉伸视图。如果你指定了一个将内容固定到边缘或角落(实际上不缩放内容)的 content mode,则视图会忽略可拉伸区域。
注意: 当为视图指定背景时,推荐使用
contentStretch
属性而非创建一个可拉伸的UIImage
对象。可拉伸视图完全由 Core Animation 层处理,这通常提供更好的性能。
内置动画支持
每一个layer对象背后都对应着一个视图,这样可以轻松地对许多与视图相关的更改进行动画处理。动画是向用户传达信息的有效方式,在设计应用程序时应始终考虑使用动画。UIView
类的许多属性都是可动画的——也就是说,存在从一个值平滑过渡到另一个值的半自动支持。要对这些可动画属性之一执行动画,你只需要:
- 告诉 UIKit 你想要执行一个动画。
- 更改属性的值。
可以在 UIView
对象上设置动画效果的一些属性包括:
frame
—— 用于动画视图的位置和大小变化。bounds
—— 用于动画视图大小的变化。center
—— 用于动画视图位置的变化。transform
—— 用于旋转或缩放视图。alpha
—— 用于改变视图的透明度。backgroundColor
—— 用于改变视图的背景颜色。contentStretch
—— 用于改变视图内容的拉伸方式。
动画在从一组视图转换到另一组视图时非常重要。通常,你使用视图控制器来管理用户界面主要部分之间转换的相关动画。例如,对于涉及从高层次信息导航到低层次信息的界面,你通常使用导航控制器来管理显示每一级数据的视图之间的转换。然而,你也可以使用动画而不是视图控制器在两组视图之间创建转换。在标准视图控制器动画无法达到预期效果的地方,你可能会这样做。
除了使用 UIKit 类创建的动画外,你还可以使用 Core Animation 层创建动画。降至层级别使你对动画的时间和属性拥有更多的控制。
视图几何与坐标系统
在 UIKit 中,默认的坐标系统原点位于屏幕的左上角,其坐标轴向右和向下延伸。坐标值使用浮点数表示,这使得无论底层屏幕分辨率如何,都可以实现精确的布局和定位。除了屏幕坐标系统外,UIWindow
和 UIView
还定义了它们自己的局部坐标系统(local coordinate systems) ,使你可以相对于视图或窗口的原点来指定坐标,而不是相对于屏幕原点。
图 1-4:UIKit 中的坐标系方向
由于每个视图和窗口都定义了自己的局部坐标系统,因此你必须清楚当前处于哪种坐标系统下:
- 当你绘制内容时,坐标是相对于该视图自身的局部坐标系统的。
- 当你更改视图的几何属性时(如 frame),坐标是相对于其父视图(superview)的坐标系统的。
UIWindow
和 UIView
类提供了多种方法用于在不同坐标系统之间进行转换,例如:
convertPoint:toView:
convertPoint:fromView:
convertRect:toView:
convertRect:fromView:
⚠️ 重要提示: 一些 iOS 技术(如 Core Graphics 和 OpenGL ES)使用的默认坐标系统与 UIKit 不同。例如,Core Graphics 使用的是左下角为原点、y 轴向上的坐标系统。你的代码在绘图或创建内容时必须考虑这些差异,并根据需要调整坐标值或坐标系统的默认方向。
Frame、Bounds 和 Center 属性的关系
视图通过以下三个关键属性来管理其大小和位置:
1. frame
- 表示视图在其父视图坐标系统中的位置和大小。
- 是一个
CGRect
结构,包含origin
(x, y)和size
(width, height)。
2. bounds
- 表示视图在其自身坐标系统中的大小和内容起点。
- 默认情况下,
bounds.origin
是(0, 0)
,bounds.size
等于frame.size
。 - 主要用于绘图操作,决定了你在哪个区域内绘制内容。
3. center
- 表示视图中心点在其父视图坐标系统中的位置。
- 如果你只想移动视图而不改变其大小,推荐使用这个属性。
图 1-5:视图的 frame 与 bounds 关系示意图
这三个属性相互关联,修改其中一个会影响其他两个:
修改属性 | 对其他属性的影响 |
---|---|
设置 frame | bounds.size 更新为新的大小,center 更新为新的中心点 |
设置 center | frame.origin 相应更新以反映新中心点 |
设置 bounds.size | frame.size 更新为相同的新大小 |
✅ 最佳实践建议:
- 如果你要仅改变视图的位置,请使用
center
。- 如果你要改变视图的位置和大小,可以使用
frame
。- 如果你要调整视图内部内容的可视区域(如滚动效果),请修改
bounds.origin
。
默认情况下,子视图不会被裁剪到父视图的边界之外,也就是说,即使子视图的一部分超出了父视图的范围,它仍然会被完整地渲染。如果你希望子视图超出部分被裁剪掉,可以将父视图的 clipsToBounds
属性设为 YES
.虽然视觉上可能会裁剪子视图,但触摸事件始终遵循目标视图的父视图的 bounds 区域。换句话说,如果某个触摸事件发生在子视图中超出父视图 bounds 的区域,则该事件不会传递给该子视图。
坐标系变换
坐标系变换提供了一种快速简便的方式来改变视图(或其内容)。仿射变换是一种数学矩阵,用于指定一个坐标系中的点如何映射到另一个不同的坐标系中的点。你可以对整个视图应用仿射变换,以改变视图相对于其父视图的大小、位置或方向。你也可以在绘图代码中使用仿射变换来对单个渲染内容进行相同类型的操控。因此,如何应用仿射变换取决于上下文:
- 要修改整个视图,请修改视图
transform
属性中的仿射变换。 - 要修改视图
drawRect:
方法中的特定内容,请修改与当前图形上下文相关联的仿射变换。
当你想要实现动画时,通常会修改视图的 transform
属性。例如,你可以使用该属性创建视图围绕其中心点旋转的动画。你不应使用此属性对视图做出永久性更改,例如在其父视图坐标空间中修改视图的位置或大小。对于此类更改,你应该直接修改视图的 frame
矩形。
注意:当修改视图的
transform
属性时,所有变换都是相对于视图中心点进行的。
在视图的 drawRect:
方法中,你可以使用仿射变换来定位和定向你计划绘制的元素。与其将某个对象固定在视图中的某个位置,更简单的方法是将每个对象相对于一个固定点(通常是 (0, 0))创建,并在绘制前使用变换来立即定位该对象。这样,如果该对象在视图中的位置发生变化,你只需修改变换,这比在新位置重新创建对象要更快且开销更小。你可以使用 CGContextGetCTM
函数获取与图形上下文关联的仿射变换,并可以使用相关的 Core Graphics 函数在绘制期间设置或修改此变换。
当前变换矩阵(CTM)是指在任何给定时刻正在使用的仿射变换。当操控整个视图的几何形状时,CTM 是存储在视图 transform
属性中的仿射变换。在 drawRect:
方法内部,CTM 是与当前图形上下文相关联的仿射变换。
每个子视图的坐标系统都建立在其祖先视图的坐标系统之上。因此,当你修改某个视图的 transform
属性时,该更改会影响该视图及其所有子视图。然而,这些更改仅影响视图在屏幕上的最终渲染效果。由于每个视图在其自身的 bounds
内绘制内容并布局其子视图,它可以在绘制和布局过程中忽略其父视图的变换。
图 1-6 演示了两个不同的旋转因子在视觉上是如何组合呈现的。在视图的 drawRect:
方法内部,对一个图形应用 45 度的旋转会导致该图形显示为旋转了 45 度。然后对该视图本身再应用一个单独的 45 度旋转,会使该图形看起来旋转了 90 度。实际上,该图形相对于绘制它的视图仍然只旋转了 45 度,但视图的旋转使其看起来像是旋转得更多。
图 1-6 旋转视图及其内容
⚠️ 重要提示:如果某个视图的
transform
属性不是单位变换,则该视图的frame
属性值是未定义的,必须被忽略。当对视图应用变换时,你必须使用视图的bounds
和center
属性来获取其大小和位置。任何子视图的frame
矩形仍然是有效的,因为它们是相对于父视图的bounds
的。
点与像素
在 iOS 中,所有的坐标值和距离都使用一种称为“点”的单位来表示,它们是浮点数值。一个点的实际大小因设备而异,并且通常无关紧要。关于点的关键理解在于,它为绘图提供了一个固定的参考框架。
表 1-1 列出了不同类型的 iOS 设备在竖屏方向下的屏幕尺寸(以点为单位)。宽度维度列在前面,随后是屏幕的高度维度。只要你根据这些屏幕尺寸设计界面,你的视图就会在相应类型的设备上正确显示。
表 1-1 不同 iOS 设备的屏幕尺寸
设备 | 屏幕尺寸(点) |
---|---|
配备 4 英寸 Retina 显示屏的 iPhone 和 iPod touch | 320 x 568 |
其他 iPhone 和 iPod touch 设备 | 320 x 480 |
iPad | 768 x 1024 |
每种设备所使用的基于点的测量系统定义了所谓的用户坐标空间。这是你在几乎所有代码中使用的标准坐标空间。例如,在操控视图的几何结构或调用 Core Graphics 函数绘制视图内容时,你都会使用点和用户坐标空间。尽管用户坐标空间中的坐标有时会直接映射到设备屏幕上的像素,但你不应假定总是如此。相反,你应该始终记住以下几点:
- 一个点不一定对应于屏幕上一个像素。
在设备层面,你指定的所有坐标最终都必须在某一点转换为像素。不过,用户坐标空间中的点到设备坐标空间中像素的映射通常由系统处理。UIKit 和 Core Graphics 都使用一种主要基于矢量的绘图模型,其中所有坐标值均使用点来指定。因此,无论底层屏幕的分辨率如何,如果你使用 Core Graphics 绘制一条曲线,你都可以使用相同的值来描述这条曲线。
当你需要处理图像或其他基于像素的技术(如 OpenGL ES)时,iOS 提供了帮助来管理这些像素。对于作为资源存储在你的应用程序包中的静态图像文件,iOS 定义了约定,允许你以不同的像素密度指定图像,并加载最匹配当前屏幕分辨率的图像。视图还提供了关于当前缩放因子的信息,以便你可以手动调整任何基于像素的绘图代码,以适应更高分辨率的屏幕。有关在不同屏幕分辨率下处理基于像素内容的技术,请参阅Drawing and Printing Guide for iOS中的 “Supporting High-Resolution Screens In Views” 部分。
视图的运行时交互模型
每当用户与界面进行交互,或者你的代码以编程方式更改某些内容时,在 UIKit 内部都会发生一系列复杂的事件来处理该交互。在该事件序列的特定时间点,UIKit 会调用视图类,并给视图响应的机会。理解这些调用点对于理解视图如何融入系统非常重要。
图 1-7 显示了从用户触碰屏幕开始,到图形系统更新屏幕内容作为响应的基本事件序列。对于任何以编程方式发起的操作,也会发生相同的事件序列。
图 1-7:UIKit 与你的视图对象的交互
以下步骤进一步分解图 1-7 中的事件序列,并解释每个阶段发生了什么以及你的应用程序可能如何响应:
-
用户触碰屏幕。
-
硬件将触摸事件报告给 UIKit 框架。
-
UIKit 框架将该触摸封装为一个
UIEvent
对象,并将其派发到适当的视图。(有关 UIKit 如何将事件传递到你的视图的详细说明,请参阅《Event Handling Guide for iOS》。) -
你的视图的事件处理代码响应该事件。例如,你的代码可能会:
- 更改视图或其子视图的属性(如
frame
、bounds
、alpha
等)。 - 调用
setNeedsLayout
方法,将视图(或其子视图)标记为需要布局更新。 - 调用
setNeedsDisplay
或setNeedsDisplayInRect:
方法,将视图(或其子视图)标记为需要重绘。 - 通知控制器某些数据的变化。
当然,决定视图应执行哪些操作以及应调用哪些方法由你决定。
- 更改视图或其子视图的属性(如
-
如果某个视图的几何结构因任何原因发生变化,UIKit 会根据以下规则更新其子视图:
- 如果你为视图配置了自动调整大小的规则,UIKit 会根据这些规则调整每个视图。
- 如果视图实现了
layoutSubviews
方法,UIKit 会调用它。
你可以在自定义视图中重写此方法,并使用它来调整任何子视图的位置和大小。例如,一个提供大范围可滚动区域的视图需要使用多个子视图作为“瓦片”,而不是创建一个不可能适应内存的大视图。在此方法的实现中,视图会隐藏当前屏幕外的子视图,或重新定位它们并用于绘制新暴露的内容。在这个过程中,视图的布局代码还可以使任何需要重绘的视图失效。
-
如果任何视图的任何部分被标记为需要重绘,UIKit 会要求视图重绘自身。
- 对于明确定义了
drawRect:
方法的自定义视图,UIKit 会调用该方法。你对该方法的实现应尽快重绘指定区域的内容,除此之外不做其他事情。此时不要进行额外的布局更改,也不要更改你的应用程序的数据模型。该方法的目的只是更新视图的视觉内容。 - 标准系统视图通常不实现
drawRect:
方法,而是在此时通过其他方式管理它们的绘制。
- 对于明确定义了
-
所有已更新的视图会被合成到应用程序的其余可见内容中,并发送到图形硬件进行显示。
-
图形硬件将渲染后的内容传输到屏幕上。
注意: 上述更新模型主要适用于使用标准系统视图和绘制技术的应用程序。使用 OpenGL ES 进行绘制的应用程序通常会配置一个全屏视图,并直接向关联的 OpenGL ES 图形上下文绘制内容。在这种情况下,该视图可能仍然处理触摸事件,但由于它是全屏的,因此不需要布局子视图。有关使用 OpenGL ES 的更多信息,请参阅OpenGL ES Programming Guide。
在上述步骤中,你自己的自定义视图的主要集成点是:
-
事件处理方法:
touchesBegan:withEvent:
touchesMoved:withEvent:
touchesEnded:withEvent:
touchesCancelled:withEvent:
-
布局方法:
layoutSubviews
-
绘图方法:
drawRect:
这些是最常被重写的方法,但你不一定需要全部重写。如果你使用手势识别器来处理事件,则无需重写任何事件处理方法。同样,如果你的视图不包含子视图或其大小不会改变,则无需重写 layoutSubviews
方法。最后,只有当你的视图内容在运行时可能发生变化,并且你使用 UIKit 或 Core Graphics 等原生技术进行绘制时,才需要 drawRect:
方法。
同时也要记住,这些是主要的集成点,但不是唯一的。UIView
类中的多个方法被设计为供子类重写的。你应该查看 UIView Class Reference 中的方法描述,以了解哪些方法可能适合你在自定义实现中重写。
有效使用视图的技巧
自定义视图在你需要绘制标准系统视图无法提供的内容时非常有用,但你有责任确保视图性能足够良好。UIKit 尽其所能优化与视图相关的行为,并帮助你在自定义视图中实现良好的性能。然而,你可以通过考虑以下建议来协助 UIKit 提升性能。
重要提示: 在优化绘图代码之前,应始终收集关于视图当前性能的数据。测量当前性能可以让你确认是否存在实际问题,如果存在,则为你提供一个基准值,以便日后对比优化效果。
视图并不总是对应一个视图控制器
在你的应用程序中,单个视图和视图控制器之间很少是一一对应的。视图控制器的职责是管理一个视图层次结构,该结构通常由多个用于实现某个独立功能的视图组成。对于 iPhone 应用程序,每个视图层次结构通常填满整个屏幕;而对于 iPad 应用程序,视图层次结构可能只填充屏幕的一部分。
在设计应用程序用户界面时,考虑视图控制器将扮演的角色非常重要。视图控制器提供了许多重要的行为,例如协调视图在屏幕上的显示、协调从屏幕上移除视图、响应内存警告释放内存,以及根据界面方向变化旋转视图。绕过这些行为可能会导致你的应用程序表现不正确或出现意外行为。
有关视图控制器及其在应用程序中角色的更多信息,请参阅《iOS 视图控制器编程指南》。
尽量减少自定义绘图
虽然有时需要进行自定义绘图,但这也是你应该尽可能避免的事情。只有在现有系统视图类无法提供你所需的外观或功能时,才真正需要进行自定义绘图。只要你的内容可以通过组合现有的视图来组装,最佳做法就是将这些视图对象组合成一个自定义的视图层次结构。
利用内容模式(Content Modes)
内容模式可以最小化重新绘制视图所花费的时间。默认情况下,视图使用 UIViewContentModeScaleToFill
内容模式,它会缩放视图的现有内容以适应视图的 frame 矩形。你可以根据需要更改此模式以调整内容显示方式,但如果可以的话,应避免使用 UIViewContentModeRedraw
模式。无论当前使用哪种内容模式,你都可以通过调用 setNeedsDisplay
或 setNeedsDisplayInRect:
强制视图重绘其内容。
只要可能,将视图声明为不透明(Opaque)
UIKit 使用每个视图的 opaque
属性来决定是否可以优化合成操作。将自定义视图的此属性设置为 YES
告诉 UIKit 不需要渲染你的视图背后的内容。这可以提升绘图代码的性能,因此建议这样做。当然,如果你将 opaque
属性设为 YES
,你的视图必须完全用完全不透明的内容填充其 bounds 矩形。
滚动时调整视图的绘图行为
滚动操作会在短时间内引发大量视图更新。如果你的视图绘图代码没有适当优化,滚动性能可能会变得迟缓。与其确保视图内容始终处于完美状态,不如在滚动操作开始时改变视图的行为。例如,在滚动过程中,你可以暂时降低渲染内容的质量,或更改内容模式。当滚动停止后,你可以将视图恢复到之前的状态并按需更新内容。
不要通过嵌入子视图来自定义控件
尽管在技术上可以向标准系统控件(继承自 UIControl
的对象)添加子视图,但你不应以这种方式自定义它们。支持自定义的控件是通过控件类本身中明确且有文档记录的接口来实现的。例如,UIButton
类包含用于设置按钮标题和背景图片的方法。使用已定义的自定义点可以确保你的代码始终正常工作。通过在按钮内部嵌入自定义图像视图或标签来绕过这些方法,可能会导致你的应用程序现在或未来按钮实现发生变化时表现出错误行为。
Windows
每个 iOS 应用至少需要一个窗口——即 UIWindow
类的一个实例,有些应用可能包含多个窗口。窗口对象有几个职责:
- 它包含了应用程序的可见内容。
- 在触摸事件传递给你的视图和其他应用程序对象中扮演关键角色。
- 与应用程序的视图控制器协作以促进方向变化。
在 iOS 中,窗口没有标题栏、关闭框或其他任何视觉装饰。窗口始终只是一个或多个视图的空白容器。此外,应用程序不会通过显示新窗口来改变其内容。当你想要更改显示的内容时,你应该更改窗口中最前面的视图。
大多数 iOS 应用在其生命周期中仅创建并使用一个窗口。此窗口覆盖设备的整个主屏幕,并且通常是从应用程序的主要 nib 文件加载(或者编程方式创建)的,在应用程序的生命早期阶段完成。然而,如果一个应用程序支持在外部显示器上输出视频,则可以创建一个额外的窗口来在该外部显示器上显示内容。所有其他窗口通常由系统创建,通常是响应特定事件创建的,例如来电。
涉及窗口的任务
对于许多应用程序而言,应用程序与其窗口交互的唯一时间是在启动时创建窗口。但是,你可以使用应用程序的窗口对象执行一些与应用程序相关的任务:
- 使用窗口对象转换点和矩形到窗口的本地坐标系或从其中转换。例如,如果你得到了一个以窗口坐标提供的值,你可能希望在使用它之前将其转换为特定视图的坐标系。有关如何转换坐标的更多信息,请参见“视图层次结构中的坐标转换”。
- 使用窗口通知跟踪窗口相关的变化。当窗口被显示或隐藏或接受或放弃关键状态时,窗口会生成通知。你可以使用这些通知在应用程序的其他部分执行操作。有关更多信息,请参见“监控窗口变化”。
创建和配置窗口
你可以编程方式或使用 Interface Builder 创建和配置应用程序的主窗口。无论哪种情况,你都应该在启动时创建窗口,并保留它并将引用存储在你的应用程序委托对象中。如果应用程序创建了额外的窗口,则应在需要时惰性地创建它们。例如,如果应用程序支持在外接显示器上显示内容,则应等到显示器连接后再创建相应的窗口。
无论你的应用程序是被启动到前台还是后台,都应该在启动时创建应用程序的主窗口。创建和配置窗口本身并不是昂贵的操作。然而,如果你的应用程序直接启动到后台,应该避免让窗口可见直到应用程序进入前台。
使用 Interface Builder 创建窗口
使用 Interface Builder 创建应用程序的主窗口非常简单,因为 Xcode 项目模板已经为你完成了这一工作。每个新的 Xcode 应用程序项目都包括一个主要 nib 文件(通常命名为 MainWindow.xib 或类似名称),其中包括应用程序的主窗口。此外,这些模板还在应用程序委托对象中定义了该窗口的出口。你使用这个出口在代码中访问窗口对象。
重要提示: 当在 Interface Builder 中创建窗口时,建议你在属性检查器中启用“Launch Full Screen”选项。如果没有启用此选项并且你的窗口小于目标设备的屏幕,某些视图将无法接收触摸事件。这是因为窗口(像所有视图一样)不会在其 bounds 矩形之外接收到触摸事件。由于视图默认不会裁剪到窗口的 bounds,因此视图仍然看起来可见,但事件无法到达它们。“Launch Full Screen”选项确保窗口大小适合当前屏幕。
如果你正在改造一个项目以使用 Interface Builder,那么只需将窗口对象拖到你的 nib 文件中即可在 Interface Builder 中创建窗口。当然,你还应该执行以下操作:
- 为了在运行时访问窗口,你应该将窗口连接到出口,通常是在你的应用程序委托或 nib 文件的所有者中定义的出口。
- 如果你的改造计划包括将新的 nib 文件设置为应用程序的主 nib 文件,你还必须在应用程序的 Info.plist 文件中设置 NSMainNibFile 键为你 nib 文件的名称。更改此键的值可确保在调用应用程序委托的
application:didFinishLaunchingWithOptions:
方法时加载 nib 文件并可供使用。
有关创建和配置 nib 文件的更多信息,请参阅 Interface Builder 用户指南。有关如何在运行时将 nib 文件加载到应用程序中的信息,请参阅资源编程指南中的 nib 文件。
以编程方式创建窗口
如果你希望以编程方式创建应用程序的主窗口,你应该在应用程序委托的 application:didFinishLaunchingWithOptions:
方法中包含如下代码:
objc
深色版本
self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];
在上面的示例中,self.window
被假定为你的应用程序委托中一个声明过的属性,并且该属性配置为保留(retain)窗口对象。如果你要为外部显示器创建窗口,则应将其赋值给其他变量,并指定代表该显示器的非主 UIScreen
对象的 bounds
。
当你创建窗口时,应该始终将窗口的大小设置为屏幕的完整边界(full bounds) 。你不需要为了状态栏或其他任何元素而减小窗口的大小。状态栏始终浮在窗口之上,因此唯一需要调整以适应状态栏的是你添加到窗口中的视图。如果你使用了视图控制器,视图控制器会自动处理视图的尺寸调整。
向窗口中添加内容
每个窗口通常有一个单一的根视图对象(由相应的视图控制器管理),该视图包含了表示你所有内容的其他子视图。使用单一的根视图可以简化更改界面的过程;要显示新内容,只需替换根视图即可。
你可以使用 addSubview:
方法将视图添加到窗口中。例如,若要添加由视图控制器管理的视图,可以使用类似以下代码:
[window addSubview:viewController.view];
作为替代方案,你也可以在 nib 文件中配置窗口的 rootViewController
属性。这种方式提供了一种更便捷的方式来通过 nib 文件设置窗口的根视图,而不是通过编程方式。如果在从 nib 文件加载窗口时设置了此属性,UIKit 会自动将与该视图控制器关联的视图安装为窗口的根视图。
你可以为窗口的根视图使用任意类型的视图。根据你的界面设计,根视图可以是:
- 一个用作多个子视图容器的通用
UIView
; - 一个标准系统视图;
- 或者是你自定义的视图。
一些常用作根视图的标准系统视图包括:滚动视图(UIScrollView
)、表格视图(UITableView
)和图像视图(UIImageView
)。在配置窗口的根视图时,你需要负责设置其在窗口中的初始大小和位置:
- 如果你的应用程序不显示状态栏,或状态栏是半透明的,则将视图的大小设置为与窗口相同。
- 如果你的应用程序显示的是不透明的状态栏,则应将视图的位置设置在状态栏下方,并相应地减小视图的高度。这样可以防止视图顶部被遮挡。
提示 如果你的窗口根视图是由容器视图控制器提供的(例如标签栏控制器
UITabBarController
、导航控制器UINavigationController
或拆分视图控制器UISplitViewController
),你无需手动设置视图的初始大小。容器视图控制器会根据状态栏是否可见自动调整其视图的大小。
改变窗口的层级(Window Level)
每个 UIWindow
对象都有一个可配置的 windowLevel
属性,它决定了该窗口相对于其他窗口的显示层级。大多数情况下,你不需要更改窗口的层级。新创建的窗口默认会被分配到“正常”层级(UIWindowLevelNormal
),这表示该窗口用于展示应用相关的内容。更高的层级被保留用于需要浮现在应用内容之上的信息,比如系统状态栏或警告消息。虽然你可以手动将窗口分配到这些层级,但通常系统会在你使用特定接口时自动完成这一操作。例如,当你显示或隐藏状态栏或弹出一个警报视图时,系统会自动创建并设置所需的窗口来显示这些内容。
Views
由于视图对象是你的应用程序与用户交互的主要方式,它们承担了许多职责。以下仅列举了一些主要功能:
- 布局与子视图管理
- 视图定义其相对于父视图的默认调整大小行为。
- 视图可以管理一个子视图列表。
- 视图可以根据需要重写其子视图的大小和位置。
- 视图可以在其自身的坐标系与其他视图或窗口的坐标系之间进行点的转换。
- 绘制与动画
- 视图在其矩形区域内绘制内容。
- 某些视图属性可以通过动画过渡到新的值。
- 事件处理
- 视图可以接收触摸事件。
- 视图参与响应链(responder chain) 。
在视图层次结构中转换坐标
在某些时候,尤其是在处理事件时,应用程序可能需要将坐标值从一个参考系转换到另一个参考系。例如,触摸事件通常以窗口的坐标系统报告每个触摸点的位置,但视图对象往往需要该信息在其自身的本地坐标系统中。
UIView
类提供了以下方法,用于在视图的本地坐标系统与其他坐标系统之间进行转换:
convertPoint:fromView:
convertRect:fromView:
convertPoint:toView:
convertRect:toView:
convert...:fromView:
方法将坐标从其他视图的坐标系统转换为当前视图的本地坐标系统(即其 bounds
矩形)。 相反地,convert...:toView:
方法将坐标从当前视图的本地坐标系统转换为指定视图的坐标系统。如果你将 nil
作为参考视图传入这些方法中的任何一个,转换将会在当前视图与包含它的窗口的坐标系统之间进行。
除了 UIView
提供的转换方法之外,UIWindow
类也定义了几个类似的坐标转换方法。它们的区别在于:这些方法是在窗口坐标系统与其他视图或自身之间进行转换。
convertPoint:fromWindow:
convertRect:fromWindow:
convertPoint:toWindow:
convertRect:toWindow:
当在旋转后的视图中进行坐标转换时,UIKit 假设你希望返回的矩形能够反映出源矩形在屏幕上所覆盖的实际区域。
图 3-3 展示了一个旋转视图中坐标转换的例子。在这个例子中,一个外层父视图包含了一个被旋转的子视图。当你将子视图中的某个矩形转换到父视图的坐标系统时,得到的是一个物理上更大的矩形。这个更大的矩形实际上是外层视图 (outerView
) 中能够完全包围旋转后矩形的最小矩形。
图 3-3:在旋转视图中进行坐标转换
这种行为确保了即使视图发生了旋转、缩放或其他变换,转换后的矩形仍然能准确表示该区域在目标坐标系统中所占据的屏幕空间。