视图和视图层级
视图基础
视图是
- UIView 的一个实例, 或它的一个子类
- 视图知道怎么绘制自己
- 能处理事件, 例如触摸(touches)
- 视图存在于视图层级中, 它的根是程序的窗口
视图层级
每个应用程序都有一个 UIWindow 的单个实例用作程序中所有视图的容器。UIWindow 是 UIView 的子类, 所以窗口自己也是一个视图。窗口在程序启动时被创建。一旦窗口创建完成, 其它视图就会被添加到窗口上。
当其它视图被添加到窗口中时, 它就是窗口的子视图。窗口的子视图还可以有子视图, 结果就是视图对象的层级, 而 window 窗口是它们的根(root)。
一旦视图层级创建完成, 它会被画到屏幕上。这个过程可以被分为2步:
- 视图层级中的每个视图, 包括窗口, 绘制自己。它们把自己渲染到它的图层上(layers), 你可以把 layers 看作一张位图。(layer 是 CALayer 的一个实例)
- 所有视图的 layers 被组合到屏幕上
视图和 Frames
当你用程序初始化一个视图时, 使用 init(frame:) 指定初始化函数。(designated initializer) 这个函数接收一个参数, 即 CGRect , 它会变成视图的 frame, 即UIView 的一个属性。
var frame: CGRect
视图的frame 指定了视图的大小和它相对于父视图的位置。因为视图的大小总是由它的 frame 指定, 视图的形状总是矩形。
CGRect 包含成员 origin
和 size
。origin
是类型为 CGPoint 的结构体, 它包含两个 CGFloat 属性: x 和 y。 size
是类型为 CGSize 的结构体, 它包含两个 CGFloat 属性: width 和 height。
在 Xcode 中新建一个叫做 WorldTrotter 的项目, 删除 ViewController.swift 中的其它方法, 只保留如下结构:
import UIKit
class ViewController: UIViewController {
}
在视图控制器的 view 被载入到内存中之后, 它的 viewDidLoad 方法会被调用。这个方法给了你自定义视图层级的机会, 所以那是一个添加你实际视图的好地方。
在 ViewController.swift 中重写 viewDidLoad 方法。创建一个 CGRect 作为 UIView 的 frame。然后创建一个 UIView 的实例, 并设置它的 backgroundColor 属性为蓝色。最后, 把 UIView 作为视图控制器的 view 的子视图添加上去以使它成为视图层级的一部分。
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let firstFrame = CGRect(x: 160, y: 240, width: 100, height: 150)
let firstView = UIView(frame: firstFrame)
firstView.backgroundColor = UIColor.blueColor()
view.addSubview(firstView)
}
}
为了创建一个 CGRect, 你要使用它的构造函数并为 origin.x 、 origin.y、size.width、size.height 传入值。
为了设置 backgroundColor, 你要使用 UIColor 的类方法 blueColor()。这是一个初始化 UIColor 实例为蓝色的便利方法。有很多 UIColor 便利方法用于普通颜色, 例如 greenColor()、blackColor() 和 clearColor()。
构建并运行该程序(Command-R)。 你会看到一个蓝色的矩形, 它就是 UIView 的一个实例。 frame 中的这些值都是点(points), 而不是像素。如果那些值是像素, 则它们在不同分辨率的设备之间会不一致(例如 Retina vs. 非 Retina)。根据显示器中像素的多少, 点也会表示多少数量的像素。尺寸、位置、线和曲线总是以点来描述的。
UIView 的每个实例都有一个 superview 属性。当你添加一个视图作为另一个视图的子视图时, 反转的关系就会自动建立。这时, UIView 的 superview 就是 UIWindow。
让我们测试下视图层级。首先, 在 ViewController.swift 中创建另外一个 UIView 实例, 使用不同的 frame 和背景色。
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let firstFrame = CGRect(x: 160, y: 240, width: 100, height: 150)
let firstView = UIView(frame: firstFrame)
firstView.backgroundColor = UIColor.blueColor()
view.addSubview(firstView)
let secondFrame = CGRect(x: 20, y: 30, width: 50, height: 50)
let secondView = UIView(frame: secondFrame)
secondView.backgroundColor = UIColor.greenColor()
view.addSubview(secondView)
}
}
现在我们调整一下视图层级。
...
let secondView = UIView(frame: secondFrame)
secondView.backgroundColor = UIColor.greenColor()
firstView.addSubview(secondView)
现在绿色视图在蓝色视图里面了。
自动布局
默认地, 每个视图有一个对齐矩形, 并且每个视图层级都使用自动布局。
对齐矩形和 frame 很相似。实际上这两个矩形经常是相似的。而 frame 包围整个视图, 对齐矩形只包围你想用于对齐意图的内容。图 3.17 展示了它俩之间的不同。
你不能直接定义 view 的对齐矩形。你没有足够的信息(例如屏幕尺寸)来做到那。相反, 你提供了一系列约束。 放在一块儿, 这些约束能使系统确定布局属性, 因此还有对齐矩形, 对于视图层级中的每个视图。
约束
不是每个布局属性都需要一个约束。如果你指定了最边距和视图的宽度, 那么视图的右边距就自动为了计算好了。
描述一个跟屏幕尺寸无关的视图的约束, 例如你想要你最上面的 label 的约束为:
- 距离屏幕最上边为 8 个点
- 在它的父视图中水平居中
- 跟它的文本同高同宽
要在 Interface Builder 中把这个描述转换为约束, 懂得怎么找到视图的最近的兄弟视图会有所帮助。最近的邻居是在指定方向上最近的兄弟视图。
如果一个视图在指定方向上没有任何兄弟视图, 那么最近的邻居就是它的父视图, 也就是作为它的容器。
现在你能讲清楚那个 Label 的约束了:
- 该 Label 的上边距应该距离它的最近的邻居(就是它的容器 — ViewController 中的 view) 8 个点。
- 该 Label 的中心应该和它的父视图的中心一样。
- 该 Label 的宽度应该和以文本字体尺寸渲染的文本的宽度一样
- 该 Label 的高度应该和以文本字体尺寸渲染的文本的高度相同。
固有内容尺寸
视图的固有内容尺寸作为显式的宽和高约束。如果你不指定明确测定宽度的约束, 那么视图的宽就是它固有的宽度。这同样适用于高度。
现在我们对这 5 个 Labels 进行自动布局。
选择最上面的那个 Label。 打开 Align 菜单并选择 Horizontally in Container, 其中约束为 0。确保 Update Frames 没有被选中; 记住不要在视图没有足够的约束之前更新 frame, 而这一个约束肯定不会提供足够的信息来计算对齐矩形。继续并添加一个约束。
在画布上选择所有 5 个 Labels。同时给多个视图添加约束也很方便。 打开 Pin 菜单并做如下选择:
- 选择最上面的 top 上边距, 设置它的约束为 8
- 从 Align 菜单中, 选择 Horizontal Centers
- 从 Updates Frames 菜单中, 选择 Items of New Constraints
约束设置完成后的界面如下: