视图和视图层级

视图和视图层级


视图基础


视图是

  • UIView 的一个实例, 或它的一个子类
  • 视图知道怎么绘制自己
  • 能处理事件, 例如触摸(touches)
  • 视图存在于视图层级中, 它的根是程序的窗口

视图层级


每个应用程序都有一个 UIWindow 的单个实例用作程序中所有视图的容器。UIWindowUIView 的子类, 所以窗口自己也是一个视图。窗口在程序启动时被创建。一旦窗口创建完成, 其它视图就会被添加到窗口上。

当其它视图被添加到窗口中时, 它就是窗口的子视图。窗口的子视图还可以有子视图, 结果就是视图对象的层级, 而 window 窗口是它们的根(root)。

一旦视图层级创建完成, 它会被画到屏幕上。这个过程可以被分为2步:

  • 视图层级中的每个视图, 包括窗口, 绘制自己。它们把自己渲染到它的图层上(layers), 你可以把 layers 看作一张位图。(layer 是 CALayer 的一个实例)
  • 所有视图的 layers 被组合到屏幕上

视图和 Frames


当你用程序初始化一个视图时, 使用 init(frame:) 指定初始化函数。(designated initializer) 这个函数接收一个参数, 即 CGRect , 它会变成视图的 frame, 即UIView 的一个属性。

1
var frame: CGRect

视图的frame 指定了视图的大小和它相对于父视图的位置。因为视图的大小总是由它的 frame 指定, 视图的形状总是矩形。

CGRect 包含成员 originsizeorigin 是类型为 CGPoint 的结构体, 它包含两个 CGFloat 属性: x 和 y。 size是类型为 CGSize 的结构体, 它包含两个 CGFloat 属性: width 和 height。

在 Xcode 中新建一个叫做 WorldTrotter 的项目, 删除 ViewController.swift 中的其它方法, 只保留如下结构:

1
2
3
4
5
import UIKit

class ViewController: UIViewController {

}

在视图控制器的 view 被载入到内存中之后, 它的 viewDidLoad 方法会被调用。这个方法给了你自定义视图层级的机会, 所以那是一个添加你实际视图的好地方。

在 ViewController.swift 中重写 viewDidLoad 方法。创建一个 CGRect 作为 UIView 的 frame。然后创建一个 UIView 的实例, 并设置它的 backgroundColor 属性为蓝色。最后, 把 UIView 作为视图控制器的 view 的子视图添加上去以使它成为视图层级的一部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
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 属性。当你添加一个视图作为另一个视图的子视图时, 反转的关系就会自动建立。这时, UIViewsuperview 就是 UIWindow

让我们测试下视图层级。首先, 在 ViewController.swift 中创建另外一个 UIView 实例, 使用不同的 frame 和背景色。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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)
}
}

现在我们调整一下视图层级。

1
2
3
4
5
6
...

let secondView = UIView(frame: secondFrame)
secondView.backgroundColor = UIColor.greenColor()

firstView.addSubview(secondView)

现在绿色视图在蓝色视图里面了。

自动布局


默认地, 每个视图有一个对齐矩形, 并且每个视图层级都使用自动布局。

对齐矩形和 frame 很相似。实际上这两个矩形经常是相似的。而 frame 包围整个视图, 对齐矩形只包围你想用于对齐意图的内容。图 3.17 展示了它俩之间的不同。

img

你不能直接定义 view 的对齐矩形。你没有足够的信息(例如屏幕尺寸)来做到那。相反, 你提供了一系列约束。 放在一块儿, 这些约束能使系统确定布局属性, 因此还有对齐矩形, 对于视图层级中的每个视图。

约束


不是每个布局属性都需要一个约束。如果你指定了最边距和视图的宽度, 那么视图的右边距就自动为了计算好了。

描述一个跟屏幕尺寸无关的视图的约束, 例如你想要你最上面的 label 的约束为:

  • 距离屏幕最上边为 8 个点
  • 在它的父视图中水平居中
  • 跟它的文本同高同宽

要在 Interface Builder 中把这个描述转换为约束, 懂得怎么找到视图的最近的兄弟视图会有所帮助。最近的邻居是在指定方向上最近的兄弟视图。

img

如果一个视图在指定方向上没有任何兄弟视图, 那么最近的邻居就是它的父视图, 也就是作为它的容器。

现在你能讲清楚那个 Label 的约束了:

  1. 该 Label 的上边距应该距离它的最近的邻居(就是它的容器 — ViewController 中的 view) 8 个点。
  2. 该 Label 的中心应该和它的父视图的中心一样。
  3. 该 Label 的宽度应该和以文本字体尺寸渲染的文本的宽度一样
  4. 该 Label 的高度应该和以文本字体尺寸渲染的文本的高度相同。

固有内容尺寸


视图的固有内容尺寸作为显式的宽和高约束。如果你不指定明确测定宽度的约束, 那么视图的宽就是它固有的宽度。这同样适用于高度。

现在我们对这 5 个 Labels 进行自动布局。

选择最上面的那个 Label。 打开 Align 菜单并选择 Horizontally in Container, 其中约束为 0。确保 Update Frames 没有被选中; 记住不要在视图没有足够的约束之前更新 frame, 而这一个约束肯定不会提供足够的信息来计算对齐矩形。继续并添加一个约束。

在画布上选择所有 5 个 Labels。同时给多个视图添加约束也很方便。 打开 Pin 菜单并做如下选择:

  1. 选择最上面的 top 上边距, 设置它的约束为 8
  2. Align 菜单中, 选择 Horizontal Centers
  3. Updates Frames 菜单中, 选择 Items of New Constraints

约束设置完成后的界面如下:

img