0%

iOS SnapKit架构之道(一)makeConstraints的过程

9月更新:(知乎体)

第一次完成这篇文章是在5月,当时是因为使用了SnapKit而不理解,所以学习了一下简单用法,大致知道了它和AutoLayout的交互。当时的题目还是“源码浅析”,在学习并结束了暑期实习之后,觉得这种标题以及自己的学习方式没有意义,于是提出了疑问:源码浅析到底是在浅析什么?目前我的答案是优雅的框架设计和优雅的Swift用法,所以更新此文,作为“架构之道”的第一篇。如果之后这个答案有了变化,就再做更新吧。


经过激烈的思想斗争,笔者从Android开始并发学习iOS,发现了两者很多的共同之处,这里就不在赘述;不过最大的不适应体现在UI方面,Android的布局编写和预览更舒适。

万般无奈之下,接触到了SnapKit,一个用Swift编写的AutoLayout框架,极大程度上简化了纯布局的代码。

本文只探究makeConstraints 的过程,介绍了基本类和它们的方法转发调用关系,也就是停留在闭包之外,对于链式调用也没有涉及到。

跨平台的ConstraintView

SnapKit的最基本用法:

1
2
3
view.snp.makeConstraints { (make) in

}

首先view.snp很容易让人想到是使用了扩展,但并不是直接对UIView的扩展,而是要引入一个新的概念ConstraintView,具体在ConstraintView.swift中体现:

1
2
3
4
5
6
7
8
9
10
11
#if os(iOS) || os(tvOS)
import UIKit
#else
import AppKit
#endif

#if os(iOS) || os(tvOS)
public typealias ConstraintView = UIView
#else
public typealias ConstraintView = NSView
#endif

这就是该文件所有的代码了,可以看到,通过判断当前系统类型,做了两件事:

  1. 包的导入:如果当前系统是iOS或者tvOS,那么导入UIKit,否则导入AppKit
  2. 类的重命名:如果当前系统是iOS或者tvOS,那么将UIView重命名为ConstraintView,否则将NSView重命名为ConstraintView。其中typealias用于为已存在的类重新命名,提高代码的可读性。

view.snp是对ConstraintView的扩展,在ConstraintView+Extensions.swift中返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#if os(iOS) || os(tvOS)
import UIKit
#else
import AppKit
#endif

public extension ConstraintView {
// 此处略去很多废弃的方法

public var snp: ConstraintViewDSL {
return ConstraintViewDSL(view: self)
}

}

注意:在SnapKit中,几乎所有文件开头都有关于导入UIKit还是AppKit的判断,之后就不再展示这段重复的代码。

此处省略了该文件中很多被废弃的方法,只看最关键的变量snp,此处返回了一个新的对象ConstraintViewDSL,并以自己,一个ConstraintView 作为参数。总而言之,ConstraintView是为了适配多平台而定义的UIViewNSView的别称,通过view.snp对所有平台的View进行操作。

中间人ConstraintViewDSL

接下来jump到ConstraintViewDSL.swift文件中,DSL意为Domain Specific Language,即领域特定语言,这里展示了常用的三个约束相关的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public struct ConstraintViewDSL: ConstraintAttributesDSL {

public func makeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
ConstraintMaker.makeConstraints(item: self.view, closure: closure)
}

public func remakeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
ConstraintMaker.remakeConstraints(item: self.view, closure: closure)
}

public func updateConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
ConstraintMaker.updateConstraints(item: self.view, closure: closure)
}

internal let view: ConstraintView

internal init(view: ConstraintView) {
self.view = view
}
}

首先可以看到ConstraintViewDSL是一个结构体,实现了ConstraintAttributesDSL接口,构造函数也非常简单,只接收一个ConstraintView 作为参数并赋给自己的成员变量保存起来;另外,view.snp.makeConstraints也只是把保存的ConstraintView ,连同传递进来的闭包一起交给ConstraintMaker 处理。

为什么说ConstraintViewDSL是中间人呢?跨平台的ConstraintView通过snp属性调用了ConstraintViewDSL,通过这个属性进行约束操作,而真正执行相应操作的是ConstraintMakerConstraintViewDSL做的工作只是转发。

操作类ConstraintMaker

就像数据库的操作类一样,ConstraintMaker的属性和方法足够帮助我们完成约束的相关操作,ConstraintMaker.swift文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class ConstraintMaker {

private let item: LayoutConstraintItem
private var descriptions = [ConstraintDescription]()

internal init(item: LayoutConstraintItem) {
self.item = item
self.item.prepare()
}

internal static func prepareConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) -> [Constraint] {
let maker = ConstraintMaker(item: item)
closure(maker)
var constraints: [Constraint] = []
for description in maker.descriptions {
guard let constraint = description.constraint else {
continue
}
constraints.append(constraint)
}
return constraints
}

internal static func makeConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) {
let constraints = prepareConstraints(item: item, closure: closure)
for constraint in constraints {
constraint.activateIfNeeded(updatingExisting: false)
}
}

}

ConstraintMaker 是一个,从上面展示的代码可以知道创建约束的基本流程:首先makeConstraints调用prepareConstraints,在prepareConstraints中构造一个maker,将maker传入闭包执行,再遍历makerdescriptions,将获取的约束添加到一个约束数组constraints中,然后prepareConstraints执行完毕并将约束返回这个constraintsmakeConstraints继续执行,获取这些约束,然后逐一激活。

构造maker时,传入构造函数的item应为保存在ConstraintViewDSL 中的ConstraintView,但在init声明中变成了LayoutConstraintItem

从ConstraintView到LayoutConstraintItem

LayoutConstraintItem.swift

1
2
3
4
5
public protocol LayoutConstraintItem: class {
}

extension ConstraintView : LayoutConstraintItem {
}

可以看到这是一个协议,并且ConstraintView实现了它,协议中也实现了一些方法,其中就包括prepare

1
2
3
4
5
6
7
extension LayoutConstraintItem {
internal func prepare() {
if let view = self as? ConstraintView {
view.translatesAutoresizingMaskIntoConstraints = false
}
}
}

prepare方法禁用了从AutoresizingMaskConstraints的自动转换,即translatesAutoresizingMaskIntoConstraints true时,可以把 frame ,bouds,center 方式布局的视图自动转化为AutoLayout实现,转化的结果就是自动添加需要的约束。用代码创建的View,该属性默认为true,而此时我们需要自己添加约束,必然会产生冲突,所以直接指定这个视图不要自动添加约束,所有约束由我们自己添加。

在创建操作类ConstraintMaker时,构造方法做了两件事:

  1. 把传入的ConstraintView参数转化为LayoutConstraintItem
  2. 调用prepare方法禁用translatesAutoresizingMaskIntoConstraints

回到操作类

到目前为止,我们知道了调用view.snp.makeConstraints时,这个view经过一系列转运,最终禁用了约束布局的自动添加,而这个过程仅仅是prepareConstraints方法的第一行,也就是只调用了ConstraintMaker 的构造函数,接下来继续分析prepareConstraints

1
2
3
4
5
6
7
8
9
10
11
12
internal static func prepareConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) -> [Constraint] {
let maker = ConstraintMaker(item: item)
closure(maker)
var constraints: [Constraint] = []
for description in maker.descriptions {
guard let constraint = description.constraint else {
continue
}
constraints.append(constraint)
}
return constraints
}

构造maker之后,先是执行了闭包的内容(不在本文讨论范围内),紧接着创建了一个包含Constraint的数组constraints;然后遍历包含了ConstraintDescription 类型的descriptions数组(该数组是maker的成员变量),并试图将每个description中包含的constraint添加到constraints数组中,最后返回该数组。

约束描述ConstraintDescription

ConstraintDescription.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ConstraintDescription {
internal lazy var constraint: Constraint? = {
guard let relation = self.relation,
let related = self.related,
let sourceLocation = self.sourceLocation else {
return nil
}
let from = ConstraintItem(target: self.item, attributes: self.attributes)

return Constraint(
from: from,
to: related,
relation: relation,
sourceLocation: sourceLocation,
label: self.label,
multiplier: self.multiplier,
constant: self.constant,
priority: self.priority
)
}()
}

此处略去了很多成员变量,简单来说,ConstraintDescription内部持有一个Constraint变量,需要时可以利这些变量构造出一个Constraint并返回。

真正的约束Constraint

Constraint.swift中,关键代码在构造函数,略去成员变量和方法,以及构造函数中关于多平台的适配之后,内容精简如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
internal init(...) {
...
self.layoutConstraints = []
// get attributes
let layoutFromAttributes = self.from.attributes.layoutAttributes
let layoutToAttributes = self.to.attributes.layoutAttributes

// get layout from
let layoutFrom = self.from.layoutConstraintItem!

// get relation
let layoutRelation = self.relation.layoutRelation

for layoutFromAttribute in layoutFromAttributes {
// get layout to attribute
let layoutToAttribute: NSLayoutAttribute
if layoutToAttributes.count > 0 {
if self.from.attributes == .edges && self.to.attributes == .margins {
switch layoutFromAttribute {
case .left:
layoutToAttribute = .leftMargin
case .right:
layoutToAttribute = .rightMargin
case .top:
layoutToAttribute = .topMargin
case .bottom:
layoutToAttribute = .bottomMargin
default:
fatalError()
}
} else if self.from.attributes == .margins && self.to.attributes == .edges {
switch layoutFromAttribute {
case .leftMargin:
layoutToAttribute = .left
case .rightMargin:
layoutToAttribute = .right
case .topMargin:
layoutToAttribute = .top
case .bottomMargin:
layoutToAttribute = .bottom
default:
fatalError()
}
} else if self.from.attributes == self.to.attributes {
layoutToAttribute = layoutFromAttribute
} else {
layoutToAttribute = layoutToAttributes[0]
}
} else {
if self.to.target == nil && (layoutFromAttribute == .centerX || layoutFromAttribute == .centerY) {
layoutToAttribute = layoutFromAttribute == .centerX ? .left : .top
} else {
layoutToAttribute = layoutFromAttribute
}
}
// get layout constant
let layoutConstant: CGFloat = self.constant.constraintConstantTargetValueFor(layoutAttribute: layoutToAttribute)

// get layout to
var layoutTo: AnyObject? = self.to.target

// use superview if possible
if layoutTo == nil && layoutToAttribute != .width && layoutToAttribute != .height {
layoutTo = layoutFrom.superview
}

// create layout constraint
let layoutConstraint = LayoutConstraint(
item: layoutFrom,
attribute: layoutFromAttribute,
relatedBy: layoutRelation,
toItem: layoutTo,
attribute: layoutToAttribute,
multiplier: self.multiplier.constraintMultiplierTargetValue,
constant: layoutConstant
)

// set label
layoutConstraint.label = self.label

// set priority
layoutConstraint.priority = self.priority.constraintPriorityTargetValue

// set constraint
layoutConstraint.constraint = self

// append
self.layoutConstraints.append(layoutConstraint)
}
}

首先创建layoutConstraints来保存最后生成的所有LayoutConstraint(继承自NSLayoutConstraint),然后获取该约束的起始对象的约束属性layoutFromAttributes和目标对象的约束属性layoutToAttributes。接下来的主要逻辑就在循环体内,通过遍历起始对象的约束属性,然后获取目标对象的约束属性,最终创建一条新的约束。

至此,prepareConstraints执行完毕,makeConstraints已经获取到了所有需要的约束,接下来要执行最后一步:激活约束。

熟悉的activateIfNeeded

这是Constraint.swift中的一个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
internal func activateIfNeeded(updatingExisting: Bool = false) {
guard let item = self.from.layoutConstraintItem else {
print("WARNING: SnapKit failed to get from item from constraint. Activate will be a no-op.")
return
}
let layoutConstraints = self.layoutConstraints

if updatingExisting {
...
} else {
NSLayoutConstraint.activate(layoutConstraints)
item.add(constraints: [self])
}
}

在其他情况下,remakeConstraints实际上是先通过removeConstraints清除之前的约束,然后再通过makeConstraints添加约束,在这一步是一样的,updatingExisting也是false。而updateConstraints调用activateIfNeeded时传入了true,在这个例子中我们先尝试理解makeConstraints的过程,即updatingExistingfalse

这里首先获取了起始目标item,类型为LayoutConstraintItem,有成员变量constraintsSet来保存所有的约束;然后获取了自己的layoutConstraints数组,此时直接调用了NSLayoutConstraint.activate激活了整个layoutConstraints数组中的约束,也就是把所有NSLayoutConstraintisActive属性设置为true,并且将这些约束添加到了起始目标的约束集合中保存起来。

之所以说熟悉,是因为在 iOS UIView绘制(三)从Layout到Display 中总结了很多关于xxxIfNeeded的方法,它们的特点都是把View标记为dirty,然后由系统在Main RunLoop中发现并进行相关操作。

总结

创建约束的过程就是先获取闭包中的约束信息(prepareConstraints),然后逐一激活(activateIfNeeded)。个人认为SnapKit是一个优雅的框架,使用和设计都比较简洁,简单总结了其中值得以后自己创造类似框架时借鉴的地方:

  • 为多平台的不同类型设计通用框架时,应该通过 typealias 等方式进行重新定义,在框架的核心操作部分使用自己定义的类(SnapKit中则为ConstraintView),做到不关心下层数据,也避免了繁琐的类型和平台判断。
  • 在框架本身和所在的底层平台交互式,尽量符合平台的设计规范和设计习惯,例如SnapKit最后激活NSLayoutConstraint时,在activateIfNeeded的方法命名中有所体现。