MENU

布局机制中的 Constraints

Cover

昨天 V 站有帖子说自动布局太过复杂,其实我想说:Apple 已经在这方面进行了大量的改进,不能还停留在老观点上。其实只要清楚的掌握了布局约束,纯代码的自动布局应用也没有想象中的那么难。那么在之前的基础上,接下来这篇文章将更为细致的讲解视图约束和布局部分的内容。

Autoresizing Constraints

自动布局作为可选机制在某些情况下会导致约束中的另一个视图也变成自动布局机制,哪怕该视图之前使用的是默认的 Autoresizing。所以这里需要一个能实现 AutoresizingAutolayout 的约束转换机制。其实 iOS 已经默认提供了该机制,我们只需通过 translatesAutoresizingMaskIntoConstraints 属性来控制该机制的开启状态(默认打开),在开启的状态下系统会自动完成 NSAutoresizingMaskLayoutConstraint 对象的转换。

下面通过两段代码来验证该转换机制。第一段代码中,使用 Autoresizing 布局创建了一个位于右上角的 UILabel

let lab1 = UILabel(frame:CGRect.init(x: 50, y: 20, width: 42, height: 22))
lab1.autoresizingMask = [.flexibleLeftMargin, .flexibleBottomMargin]
lab1.text = "Hello"
self.view.addSubview(lab1)

旋转设备之后能够发现布局机制已经生效了。

接下来我们使用 Autolayout 创建另一个位于 lab1 的正下方的 UILabel

let lab2 = UILabel()
lab2.translatesAutoresizingMaskIntoConstraints = false
lab2.text = "Howdy"
self.view.addSubview(lab2)
NSLayoutConstraint.activate([
    lab2.topAnchor.constraint(equalTo: lab1.bottomAnchor, constant: 20),
    lab2.trailingAnchor.constraint(
        equalTo: self.view.trailingAnchor, constant: -20)
])

这段代码中因为约束定义中使用了 lab1 相关的属性,所以导致 lab1 被迫转换布局机制为Autolayout 。 并且 lab1translatesAutoresizingMaskIntoConstraints 使用的是默认的 true ,这将使 lab1 原本的布局约束自动发生了转换。相反,若该机制未开启的话那么布局将会出现错误。

注意:如果直接使用代码创建视图并且应用 Autolayout 机制,那么该视图的 translatesAutoresizingMaskIntoConstraints 需要设置为 false 。这样能够避免与 Autoresizing 之间可能的冲突。

Creating Constraints in Code

下面再来看看 NSLayoutConstraint 部分的内容。其实除了在 IB 中定义约束外,我们还可以手动通过代码来创建 NSLayoutConstraint 实例:

init(item: attribute: relatedBy: toItem: attribute: multiplier: constant: )

下面我们创建和上一部分同样效果的不同代码实现:

let v1 = UIView(frame:CGRect.init(x:100, y:111, width:132, height:194))
v1.backgroundColor = UIColor(red: 1, green: 0.4, blue: 1, alpha: 1)
let v2 = UIView()
v2.backgroundColor = UIColor(red: 0.5, green: 1, blue: 0, alpha: 1)
let v3 = UIView()
v3.backgroundColor = UIColor(red: 1, green: 0, blue: 0, alpha: 1)
self.view.addSubview(v1)
v1.addSubview(v2)
v1.addSubview(v3)
v2.translatesAutoresizingMaskIntoConstraints = false
v3.translatesAutoresizingMaskIntoConstraints = false

接下来设置视图的约束:

v1.addConstraint(
    NSLayoutConstraint(item: v2,
        attribute: .leading,
        relatedBy: .equal,
        toItem: v1,
        attribute: .leading,
        multiplier: 1, constant: 0)
)
v1.addConstraint(
    NSLayoutConstraint(item: v2,
        attribute: .trailing,
        relatedBy: .equal,
        toItem: v1,
        attribute: .trailing,
        multiplier: 1, constant: 0)
)
v1.addConstraint(
    NSLayoutConstraint(item: v2,
        attribute: .top,
        relatedBy: .equal,
        toItem: v1,
        attribute: .top,
        multiplier: 1, constant: 0)
)
v2.addConstraint(
    NSLayoutConstraint(item: v2,
        attribute: .height,
        relatedBy: .equal,
        toItem: nil,
        attribute: .notAnAttribute,
        multiplier: 1, constant: 10)
)
v3.addConstraint(
    NSLayoutConstraint(item: v3,
        attribute: .width,
        relatedBy: .equal,
        toItem: nil,
        attribute: .notAnAttribute,
        multiplier: 1, constant: 20)
)
v3.addConstraint(
    NSLayoutConstraint(item: v3,
        attribute: .height,
        relatedBy: .equal,
        toItem: nil,
        attribute: .notAnAttribute,
        multiplier: 1, constant: 20)
)
v1.addConstraint(
    NSLayoutConstraint(item: v3,
        attribute: .trailing,
        relatedBy: .equal,
        toItem: v1,
        attribute: .trailing,
        multiplier: 1, constant: 0)
)
v1.addConstraint(
    NSLayoutConstraint(item: v3,
        attribute: .bottom,
        relatedBy: .equal,
        toItem: v1,
        attribute: .bottom,
        multiplier: 1, constant: 0)
)

看完代码后,我能猜到你的第一感觉一定是:“搞事么?代码这么太冗长了。” 但是上面的代码其实很相似,完全可以通过拷贝复制然后修改一下参数来减少工作量。

更为重要的是这些约束与手动设置 frameAutoresizing 相比,更清楚地表达视图之间的关系。我们不必在每次视图发生变化的时候使用下面代码中的数字表示其间的几何关系:

v3 = UIView(frame:CGRect.init(x: v1.bounds.width-20, y: v1.bounds.height-20, width: 20, height: 20))

这些数字对于视图关系表诉远没有约束部分代码直接。另外约束可以完成 Autoresizing 不能做的事情,例如,将 v2 绝对高度值改为 v1 的高度的十分之一。

Anchor notation

不得不承认上面的代码确实有点冗长了,以至于 Apple 自己都看不下去了。所以在 iOS 9 中引入了一种更加紧凑的形式来完成常用的约束定义。当然这种代码简洁性肯定是伴随这一定的妥协的。新的描述方法不如上一部分那么灵活,它的关注点集中在约束中的属性:

  • widthAnchor, heightAnchor

  • topAnchor, bottomAnchor

  • leftAnchor, rightAnchor, leadingAnchor, trailingAnchor

  • centerXAnchor, centerYAnchor

  • firstBaselineAnchor, lastBaselineAnchor

这些锚属性都是 NSLayoutAnchor 或者其子类的实例,你可以根据实际情况对其进行设置。另外这些实例中的 multiplier 默认为 1 而 constant 默认为 0。而每组约束中的数学关系不外乎三种:大于、等于、小于,Apple 也为我们提供了以下一组函数:

  • constraint(equalTo:)

  • constraint(greaterThanOrEqualTo:)

  • constraint(lessThanOrEqualTo:)

  • constraint(equalTo:constant:)

  • constraint(greaterThanOrEqualTo:constant:)

  • constraint(lessThanOrEqualTo:constant:)

  • constraint(equalTo:multiplier:)

  • constraint(greaterThanOrEqualTo:multiplier:)

  • constraint(lessThanOrEqualTo:multiplier:)

  • constraint(equalTo:multiplier:constant:)

  • constraint(greaterThanOrEqualTo:multiplier:constant:)

  • constraint(lessThanOrEqualTo:multiplier:constant:)

  • constraint(equalToConstant:)

  • constraint(greaterThanOrEqualToConstant:)

  • constraint(lessThanOrEqualToConstant:)

这些函数已经非常清晰的表明了约束中双方的数学关系,但是并没有表明约束的从属。所以这里需要使用类方法 activate(_:) ,让系统自行进行判断。精简后的代码为:

NSLayoutConstraint.activate([
    v2.leadingAnchor.constraint(equalTo:v1.leadingAnchor),
    v2.trailingAnchor.constraint(equalTo:v1.trailingAnchor),
    v2.topAnchor.constraint(equalTo:v1.topAnchor),
    v2.heightAnchor.constraint(equalToConstant:10),
    v3.widthAnchor.constraint(equalToConstant:20),
    v3.heightAnchor.constraint(equalToConstant:20),
    v3.trailingAnchor.constraint(equalTo:v1.trailingAnchor),
    v3.bottomAnchor.constraint(equalTo:v1.bottomAnchor)
])

八行代码定义了的八个约束,然后激活这些约束后它们就自动添加到界面中了。 另外,立即激活所有视图约束的操作并不是必须,但最好还是建议这样做。

TIP:在 iOS 10 中如果您需要分析代码中的现有约束,您可以读取其firstAnchor和secondAnchor属性,而不是更麻烦的firstItem,firstAttribute,secondItem和secondAttribute。

Visual format notation

缩写约束条件的另一种方法是使用一种基于文本标记的格式,称为 visual format。在文本标记中我们可以在水平或者垂直方向上同时描述多个约束的优点,这将进一步精简代码。例如:“V:| [v2(10)]” 这个表达式中,V:表示垂直方向,对应的就是水平方向 H (默认值)。视图的名称显示在方括号中,管道(|)表示父视图,所以该段文本描述的是 v2 与父视图顶部对齐。括号里面的数字表明该方向上的尺寸,因此v2的高度被设置为10。

要使用视觉格式,您必须提供一个字典,将在约束中涉及到的是图进行编码从而避免硬编码形式代码的存在。例如,伴随前面表达式的字典可能是[“v2”:v2]。所以这里是另一种表达前面约束代码示例的方式,使用visual format 的缩写:

let d = ["v2":v2, "v3":v3]
NSLayoutConstraint.activate([
    NSLayoutConstraint.constraints(withVisualFormat:
        "H: | [v2] |", metrics: nil, views: d),
    NSLayoutConstraint.constraints(withVisualFormat:
        "V: | [v2(10)]", metrics: nil, views: d),
    NSLayoutConstraint.constraints(withVisualFormat:
        "H: [v3(20)] |", metrics: nil, views: d),
    NSLayoutConstraint.constraints(withVisualFormat:
        "V: [v3(20)] |", metrics: nil, views: d)
].flatMap{$0})

这里使用了四个命令而不是八个,代码更进一步得到精简。withVisualFormat:...类方法产生一个约束数组,但是 activate(_:) 期望一个数组的约束,所以这里进行了 flatMap 处理。

当多个视图在同一方向上相互关联时,视觉格式语法将发挥出最大的效果。在这种情况下,你可以一次将这些相关联的约束全部创建完成。当然,缺陷也是显而易见的。它隐藏了它产生的约束的数量和相关约束中的属性并且该方式较之前在表达上也更容易出错,但是我们还是有必要熟悉视觉格式的语法。

使用视觉格式语法生成约束时,还有一些其他的事情需要注意:

  • metrics:字典类型变量主要用于对约束中常量进行编码,这样就可以像 views 一样在代码中避免硬编码。

  • options:是一个位掩码(NSLayoutFormatOptions),主要用于设置对齐方式。

  • 在指定两个连续视图之间的距离是需要使用连字符格式,如:“[v1] -20- [v2]”。数值可以可选地被括号括起来。

  • 括号中的数值可能前面有一个相等或不等式运算符,后面跟着一个具有优先级的 @ 符号。多个数值以逗号分隔,可以一起显示在括号中。例如:“[v1(> = 20 @ 400,<= 30)]”。

有关视觉格式语法的正式详细信息,请参阅 Auto Layout Guide视觉格式语法一章,也可以看看Raywenderlich 的这篇文章

Layout guides

到目前为止,所有的约束都是我们基于视图之间的关系进行定义的,例如:

let c = v2.leadingAnchor.constraint(equalTo: v1.leadingAnchor)

但是对于布局机制来说这些还远远不够便利,所以 Apple 为了提升布局效率提供了很多辅助性质的布局锚点。这些辅助型的属性伴随着 iOS 发展也多次发生变化,下面让我们看看这部分的内容。

在工程中子视图主要固定在根视图的可视区域之间,通常情况下这些可视区域就是状态栏、导航栏、标签栏之间的的区域。然而从 iOS 7 开始,根视图可以垂直延伸到这些栏后面的窗口边缘。换句话说也就是状态栏、导航栏、标签栏都具有穿透效果是浮在根视图上面的(UIScrollView 及其之类布局差异与之相关,有机会再讲)。另外,这些 Bar 的状态是可变化的,例如:iOS 8 之后,设备横屏会导致状态栏消失。

因此还需要其他东西,以锚定其子视图的垂直约束 - 动态的响应当前变化已作出正确的改变。 否则,在某些情况下看起来是正确但是在其他情况下会出现错误。

为了解决这个问题,UIViewController提供和维护两个不可见的视图,顶部的布局指南和底部布局指南,它作为子视图注入其主视图的视图层次结构。所以子视图的垂直约束通常已经不在主视图的顶部或底部之间,而是这两个布局指南的底部之间。最重要的是,这些布局指南会根据情况发生变化而改变其大小 - 顶部或底部栏杆会改变其高度,或完全消失,因此子视图被限制在主视图的可视区域之间。

例如,我们希望当设备旋转前后视图 v 始终与根视图的顶部对齐:

let tlg = self.topLayoutGuide
let c = v.topAnchor.constraint(equalTo: tlg.bottomAnchor)

除了这一个应用场景外,我们还可以在 View 中对其加以利用。例如,我们可以通过创建 UILayoutGuide 来手动创建类似于 UIStackView 的视图:

let guides = [UILayoutGuide(), UILayoutGuide(), UILayoutGuide()]
for guide in guides {
    self.view.addLayoutGuide(guide)
}

NSLayoutConstraint.activate([
    // guide left is arbitrary, let's say superview margin 
    guides[0].leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
    guides[1].leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
    guides[2].leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
    // guide widths are arbitrary, let's say 10
    guides[0].widthAnchor.constraint(equalToConstant: 10),
    guides[1].widthAnchor.constraint(equalToConstant: 10),
    guides[2].widthAnchor.constraint(equalToConstant: 10),
    // bottom of each view is top of following guide 
    views[0].bottomAnchor.constraint(equalTo: guides[0].topAnchor),
    views[1].bottomAnchor.constraint(equalTo: guides[1].topAnchor),
    views[2].bottomAnchor.constraint(equalTo: guides[2].topAnchor),
    // top of each view is bottom of preceding guide
    views[1].topAnchor.constraint(equalTo: guides[0].bottomAnchor),
    views[2].topAnchor.constraint(equalTo: guides[1].bottomAnchor),
    views[3].topAnchor.constraint(equalTo: guides[2].bottomAnchor),
    // guide heights are equal! 
    guides[1].heightAnchor.constraint(equalTo: guides[0].heightAnchor),
    guides[2].heightAnchor.constraint(equalTo: guides[0].heightAnchor),
])
标签: Layout