SnapKit 源码阅读

SnapKit

SnapKit 是一个针对 iOS AutoLayout 的 Swift 版的 Domain Specific Language (DSL),它提供了一个 Swift 版的,对 AutoLayout 的良好封装,让 AutoLayout 更加平易近人。

Basic Usage

阅读源码前一定要先了解它的用法,我们才可以去探究它的实现。

以上图为例,View 的顶边底边,左边都是相对 Container 的对应边 20,View的右边距离 Button 的左边 20,高度为 50

用 SnapKit 描述就是

1
2
3
4
5
6
view.snp.makeConstraints { make in
make.left.top.equalTo(container).offset(20.0)
make.bottom.equalTo(container).offset(-20)
make.right.equalTo(button.snp.left).offset(-20)
make.height.equalTo(50)
}

以上就是基本用法,如此的设计大大减少了我们的代码量。

Get Started

snp

SnapKit 比较优雅的一点就是使用了 snp 来实现非侵入式的拓展。

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

源代码中用 snp 这个计算变量用当前 View 初始化了一个 ConstraintViewDSL 结构体,然后所有的操作就跳脱了 View 的拓展,把工作都交给了 ConstraintViewDSL

这样的好处是可以避免写更多 View 的拓展,减少了模块对 View 拓展的依赖。

ConstraintViewDSL

首先这个被声明成了结构体而不是类,其次,综管其上下,内部其实只有一个变量,即该 创建该结构体的类。
其中的关键方法

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

这里把构建约束的工作交给了 ConstraintMaker 这个我们待会再说。

我们看到使用的时候发现有一个 button.snp.left 的写法,这个的实现放在了 ConstraintViewDSL 遵循的协议 ConstraintAttributesDSL 的继承协议 ConstraintBasicAttributesDSL 的拓展实现中。具体实现如下:

1
2
3
public var left: ConstraintItem {
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.left)
}

这个计算属性创建了一个 ConstraintItem,现在可以理解为一个约束的一部分,描述了一个约束的一部分,比一个对象的 left、right 等等。

ConstraintMaker

我们回到 ConstraintMaker ,核心方法:

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
// Note: 这里是静态的方法
internal static func prepareConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) -> [Constraint] {
// 这里创建了 ConstraintMaker 的实例
let maker = ConstraintMaker(item: item)
// 这个内部便是我们配置 maker 的过程
closure(maker)
var constraints: [Constraint] = []
// 提取配置好的约束描述
for description in maker.descriptions {
// description.constraint 便生成了对应的约束(懒加载)
guard let constraint = description.constraint else {
continue
}
constraints.append(constraint)
}
return constraints
}
// Note: 这里是静态方法
internal static func makeConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) {
// 调用 prepareConstraints 来配置约束
let constraints = prepareConstraints(item: item, closure: closure)
// 这里将所有约束全都加在 view 上
for constraint in constraints {
constraint.activateIfNeeded(updatingExisting: false)
}
}

整个流程还是很明确的,但是核心的点就在 closure(maker) 这一句,是通过对 maker 的何种调用来实现这一切的。注意,这里的 item 是调用 snp 的那个对象。

核心就在 maker 的实例方法上。往上翻,就可以发现 maker 的实例方法。举两个简单的如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public var left: ConstraintMakerExtendable {
return self.makeExtendableWithAttributes(.left)
}
public var top: ConstraintMakerExtendable {
return self.makeExtendableWithAttributes(.top)
}
internal func makeExtendableWithAttributes(_ attributes: ConstraintAttributes) -> ConstraintMakerExtendable {
let description = ConstraintDescription(item: self.item, attributes: attributes)
self.descriptions.append(description)
return ConstraintMakerExtendable(description)
}

我们发现这几个计算变量都调用了 makeExtendableWithAttributes() 生成了一个 ConstraintDescription 对象,并返回了一个 ConstraintMakerExtendable 对象。

ConstraintDescription

这是一个类,全部内容如下:

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
public class ConstraintDescription {
// 发起约束的对象
internal let item: LayoutConstraintItem
// 对对象的描述,如 left、right 等等
internal var attributes: ConstraintAttributes
// 约束的相关性,是对 Cocoa 的 LayoutRelation 进行的封装
internal var relation: ConstraintRelation? = nil
internal var sourceLocation: (String, UInt)? = nil
internal var label: String? = nil
// 约束的另一半
internal var related: ConstraintItem? = nil
// AutoLayout 的三个基本配置
internal var multiplier: ConstraintMultiplierTarget = 1.0
internal var constant: ConstraintConstantTarget = 0.0
internal var priority: ConstraintPriorityTarget = 1000.0
// 生成 SnapKit 中的约束对象,用于生成最终 AutoLayout 的约束对象
internal lazy var constraint: Constraint? = {
guard let relation = self.relation,
let related = self.related,
let sourceLocation = self.sourceLocation else {
return nil
}
// 这里生成约束的集合描述,即将 item 和 attribute 包装在一起
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
)
}()
// MARK: Initialization
internal init(item: LayoutConstraintItem, attributes: ConstraintAttributes) {
self.item = item
self.attributes = attributes
}
}

我们看到在 ConstraintMakerExtendable 中有如下的调用

1
self.description.attributes += .left

这种写法就是 Swift 的 OptionSet 写法,类似于 OC 中的 xxx|yyy 写法,但是 Swift 中可以用 [.xxx, .yyy] 的写法,更优雅,可读性更高。

ConstraintMakerExtendable

ConstraintMakerExtendable 是一个类,继承与 ConstraintMakerRelatable, 一个可以表示相关性的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ConstraintMakerRelatable {
internal let description: ConstraintDescription
internal init(_ description: ConstraintDescription) {
self.description = description
}
internal func relatedTo(...) -> ConstraintMakerEditable {...}
// 下面的方法都都调用了上面这个全能方法:relatedTo 表示了约束之间的关系
public func equalTo(_ other: ConstraintRelatableTarget, ...) {...}
func equalToSuperview(...) {...}
func lessThanOrEqualTo(...) {...}
// ....
}

然后对其子类 ConstraintMakerExtendable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ConstraintMakerExtendable: ConstraintMakerRelatable {
public var left: ConstraintMakerExtendable {
self.description.attributes += .left
return self
}
public var top: ConstraintMakerExtendable {
self.description.attributes += .top
return self
}
// ....
}

则是往 description 中添加东西,然后返回自己以提供链式调用。
所以我们就要关注一下 description,它做了什么可以储存遮这样的信息,以及他为什么叫 description

Trick


equalTo 的地方有一个很有意思的 Trick,它的类型是 ConstraintRelatableTarget,这样写的目的是我们的 api 也是支持 make.width.height.equalTo(50) 这样的写法。这里的 50 并不是一个 view 或者一个 ConstraintItem 所以需要一个类型将它们统一起来。所以就有了如下的代码:

1
2
3
4
5
6
7
8
9
10
public protocol ConstraintRelatableTarget {
}
extension Int: ConstraintRelatableTarget {
}
// 略去其他想 Float 等类型....
extension ConstraintItem: ConstraintRelatableTarget {
}
extension ConstraintView: ConstraintRelatableTarget {
}

这样一个 Trick 让很多不同个类型都统一了起来,成为一个抽象的 ConstraintRelatableTarget 进行使用,如此来支持多种类型。这样就避免为了支持多类型而是用 Any 这个类型不安全的类型,也做到了很好的类型约束,可以在编译期就能检查出类型的错误。

因为在 iOS 9 中引入了一个新的 UILayoutGuide,它在参与 AutoLayout 布局时与 view 有着同样的作用,所以也出如下的代码

1
2
3
4
5
6
7
8
9
public protocol LayoutConstraintItem: class {
}
@available(iOS 9.0, OSX 10.11, *)
extension ConstraintLayoutGuide : LayoutConstraintItem {
}
extension ConstraintView : LayoutConstraintItem {
}

这里也是将两者合并为了 LayoutConstraintItem


然后我们继续看到 relatedTo() 函数返回了一个 ConstraintMakerEditable 类型。

ConstraintMakerEditable

类似 ConstraintMakerExtendable, 这个类配置了 descriptionmultiplier offset inset dividedBy 属性。
ConstraintMakerExtendable 用了同样的技巧来规避内容的调用顺序,即继承,它继承了 ConstraintMakerPriortizable

ConstraintMakerPriortizable

这个类配置了 description 的优先级 priority ,然后在用继承,继承了 ConstraintMakerFinalizable

ConstraintMakerFinalizable

这个类配置了 label 值,一个用于对约束的描述,调试使用。

这三个类使用继承,这三个类的属性可以以任意的顺序进行调用,然后 ConstraintMakerEditableConstraintMakerExtendable 分离,又很好的强调了描述约束的顺序要求。

简单的小结

1
maker.top.left.equalTo(button.snp.right).labeled("test").priority(.high)

(这个约束加的没有意义,只为举个栗子)
``第一个 top 生成了一个 ConstraintMakerExtendable,之后的 left 则是对 ConstraintMakerExtendable 的调用,向 description 中添加一个新的属性,之后的 equalTo 调用的是其父类的方法。而后返回的才可以配置约束的一些常数部分。语法和语义进行了很好的匹配。

我们之前也探究了 snp.right 的写法,是直接生成了一个 ConstraintItem,这个是不支持链式调用的。

ConstraintMakerExtendable 这个类提供了链式调用的写法来配置 description

我们每调用次以 maker 的实例方法都会创建一个 descriptionmaker 会帮我们记录下来。

再次回到 ConstraintMaker

现在已经配置好了 description,接下来就在 prepareConstraints() 中提取出所有的 constraint 交还给 makeConstraints(),然后对所有的 constraint 调用 constraint.activateIfNeeded(updatingExisting: false)

Constraint

现在终于来到了 Constraint 类,这个类是 SnapKit 对约束的一个包装,他负责生成真正的 NSLayoutConstraint 的子类 LayoutConstraint,它提供的功能也不多,只是判断两个约束的相等性以及包装 NSLayoutConstraint.identifier 和持有一个生成它的 Constraint 的弱指针,方便查找。

这个转换在 init() 中就已经完成。这个转换要注意,如果一个对象的 attributes 中如果有多个,比如有 left 以及 right,它就会生成两个 LayoutConstraint 对象并储存起来。所以如果要提取约束 left 用于之后控制的话,最好不要使用

1
2
3
4
5
6
7
8
9
10
self.constraint = make.left.right.equalTo(container).constraint
```
来获取约束,否则就要使用
```swift
self.constraint.layoutConstraints[0]
```
来获取对应约束。这样操作有顺序风险,并且 `self.constraint.activate` 对其下对所有的约束都有应用,所以不推荐。建议分开写成:
```swift
self.constraint = make.left.equalTo(container).constraint
make.right.equalTo(container)

常数处理

SnapKit 中使用了很多像上面的 Trick 部分的技巧,包装了很多数值类型,将其拓展不同的类型供不同部分使用,所有的封装如下:

Target 描述
ConstraintRelatableTarget relatedTo 使用
ConstraintConstantTarget offset等使用
ConstraintPriorityTarget priority使用,因为要使用 CGFloat
ConstraintMultiplierTarget multiplier 使用,理由同上
ConstraintOffsetTarget offset 使用
ConstraintInsetTarget inset 使用

Summary

总结下来,SnapKit 也是用了很多技巧使得这个库使用起来简单而优雅。

首先是使用 ConstraintViewDSL 产生一个对象来跳脱 UIView。

使用了 protocol 来整合基础类型,作为一种参数方便的使用。

使用了继承来分模块来拓展支持属性,并且利用返回自己来实现链式调用。

合理的控制对象类型的顺序来实现对象的控制语法和语义。