这是对 Swift 布局框架 SnapKit 的源码的一点分析,尝试搞清,一个好的布局框架,背后都做了些什么。
ConstraintView
等同于 UIView
ConstraintAttributes
用于构造约束关系的各种元素(上下左右等)
ConstraintDescription
包含了包括 ConstraintAttributes 在内的各种与约束有关的元素,一个 ConstraintDescription 实例,就可以提供与一种约束有关的所有内容。
ConstraintMaker
构造约束关系的起点,提供了 makeConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void)
方法来为程序员提供了描述约束的空间,也可以通过 left right top bottom centerX centerY 等属性,去生成一个 ConstraintMakerExtendable 实例(见下面)
ConstraintMakerExtendable(继承 ConstraintMakerRelatable)
提供 left right top bottom leading trailing edges size margins 等内容,用以产生一个 ConstraintMakerRelatable 类型的实例
ConstraintMakerRelatable
直接用于构造约束关系,也是常用方法 equalTo(_ other: ConstraintRelatableTarget) -> ConstraintMakerEditable
与 equalToSuperview
的来源。核心方法是 relatedTo(_ other: ConstraintRelatableTarget, relation: ConstraintRelation, file: String, line: UInt) -> ConstraintMakerEditable
,返回 ConstraintMakerEditable 类型的实例
ConstraintMakerEditable(继承 ConstraintMakerPriortizable)
在设定约束的宽度、高度以及偏移的时候,提供相应的加减乘除方法,返回 ConstraintMakerPriortizable 类型的实例
ConstraintMakerPriortizable(继承 ConstraintMakerFinalizable)
提供方法来设置约束的 priority,返回 ConstraintMakerFinalizable 类型的实例
ConstraintMakerFinalizable
一个只有一个类型为 ConstraintDescription 的属性的类,正如它的类名,有一个 ConstraintMakerFinalizable 实例,就得到了对于一个约束的完整描述。
至此,我们已经知道 SnapKit 是靠什么来确定了三个东西:
let aView = UIView() aView.snp.makeConstraints({ make in make.width.equalToSuperview().dividedBy(2).priority(100) })
当我们写下这样的语句时,先忽略掉 snp
是什么不管,里面设定 aView 的宽度为它的父视图的一半的这行约束语句,执行了这样的逻辑:
makeConstraints
方法来让我们写约束的同时,开始维护了一个 ConstraintDescription 数组,叫 descriptions
.width
时, descriptions
数组第一次加入内容( self.description
),同时我们用这个内容生成了一个 ConstraintMakerRelatable 实例 .equalToSuperview()
时,上一步中的内容( self.description
)继续添加信息,同时我们用它生成了一个 ConstraintMakerEditable 实例 .dividedBy(2).priority(100)
使得之前的 ConstraintMakerEditable 实例变成了一个 ConstraintMakerFinalizable 实例,这个实例的 description 属性的类型是 ConstraintDescription,它包含了我们所描述的全部内容。但由于 ConstraintMakerEditable 本身就继承自 ConstraintMakerFinalizable,所以 .dividedBy(2).priority(100)
这一部分即便不写,这条语句在语法上也已经完成。 做个总结:到这里我们发现 ConstraintMaker 以及和它相关的类,构造了一套 DSL 来让我们可以轻松地写出约束语句,而这些语句把信息都放到了一个 ConstraintDescription 实例( self.description
)里面,但我们仍然不知道它是如何以 UIKit 里面的 NSLayoutConstraint 的形式作用的。
SnapKit 里面存在这样一些东西:
public protocol ConstraintDSL {}
public protocol ConstraintBasicAttributesDSL : ConstraintDSL {}
public protocol ConstraintAttributesDSL : ConstraintBasicAttributesDSL {}
public struct ConstraintViewDSL: ConstraintAttributesDSL {}
上面我们知道了 aView
作为一个 UIView,它同时也就是一个 ConstraintView,ConstraintView 有一个 snp 的属性,这给我们提供了入口来通过 SnapKit 给任意的 UIView 或 AppKit 里面的 NSView 通过 .snp
这样的语法来写约束。
一看就是面向协议的写法,通过一个个的 extension 来给 protocol 添加功能,最后用 struct 实现出来,就有了 snp 这个属性。
let topView = UIView() let centerView = UIView() centerView.snp.makeConstraints({ make in make.top.equalTo(topView.snp.bottom).offset(16) })
这段代码展现了 snp 的两个作用:
prepareConstraints
makeConstraints
remakeConstraints
updateConstraints
removeConstraints
等函数,我们最常用的是 makeConstraints
,传入一个 closure,在里面写约束关系。这里要注意,我们使用的 makeConstraints
方法来源于 ConstraintViewDSL,但真正实现了构造约束的其实是我们上文里面写的 ConstraintMaker 里面的 makeConstraints
方法,见图: 到现在我们还是没说,从 snp 到 ConstraintMaker,再到 ConstraintMakerFinalizable 的 description 属性,到底哪里创建了 NSLayoutConstraint ,答案其实在之前提过多次的 ConstraintMaker 里面
// public class ConstraintMaker internal static func makeConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) { 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) } for constraint in constraints { constraint.activateIfNeeded(updatingExisting: false) } } internal static func updateConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) { guard item.constraints.count > 0 else { self.makeConstraints(item: item, closure: closure) return } 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) } for constraint in constraints { constraint.activateIfNeeded(updatingExisting: true) } }
我们传入一个闭包来写约束关系时,这个闭包给叫做 maker 的 ConstraintMaker 实例写入了信息,遍历 maker 的 descriptions 之后(我们之前说一条约束语句最终得到一个 self.description,但往往会有多条约束,所以 ConstraintMakerFinalizable 里面的 self.description,在 ConstraintMaker 里被一个数组维护),我们得到了 Constraint 数组。
Constraint 这个类还没有介绍过,不过上面这个核心方法加上以前的内容,已经可以让我们猜出来,约束是怎么写出来的了:
随便写了两句,展示一下各个方法传入的参数的类型,发现有各种 Target,貌似很复杂,不过点开之后发现是这种景象:
说白了就是因为 equalTo:
这个方法里面能传的参数类型比较多,手动来一个一个限制一下,我们看到 ConstraintRelatableTarget 这里可以放一些原生的可以代表数字的类型,外加四个自定义的 Constraint 类型。其他的 Target 协议也差不多是这种情况。
个人觉得这种做法还是挺值得学习的。
SnapKit 里面用来表示位置主体的类其实不是 ConstraintView,而是 ConstraintItem
我们管这个“主体”叫 target,一个 target,再加上一个 ConstraintAttributes 实例,就可以组成一个 ConstraintItem。
有 attributes 属性很好理解,因为比如我们去做对齐,可以是 aView 的 top 和 bView 的 bottom 对齐,而不能是 aView 和 bView 对齐。但是为什么 target 的类型是 AnyObject 而不是 ConstraintView,即 UIView 或 NSView 呢?
在 ConstraintViewDSL 里面,target 确实是 ConstraintView 类型,
但在 ConstraintLayoutSupportDSL 里面,target 是 ConstraintLayoutSupport 类型,
在 ConstraintLayoutGuideDSL 里面,target 是 ConstraintLayoutGuide 类型
这部分就不具体解释了,想一探究竟的去看 LayoutConstraintItem.swift 这个文件吧。