官方Github说明上的实例代码如下:
1 | import SnapKit |
snp是什么
它定义在ConstraintView+Extensions.swift
中,是一个对ConstraintView
的扩展,ConstraintView
根据对应的平台不同代表UIView
或者NSView
1 | #if os(iOS) || os(tvOS) |
统一各平台View
的差异性,定义了一个统一的别名。
1 | public extension ConstraintView { |
该扩展为UIView
扩展了一个名叫snp
的计算型属性,返回一个持有了view
实例的ConstraintViewDSL
对象,结合官方给的例子,就是持有了box
的ConstraintViewDSL
对象。
makeConstraints如何执行的
ConstraintViewDSL
是一个实现了ConstraintAttributesDSL
的协议的结构体,ConstraintAttributesDSL
协议又继承自ConstraintBasicAttributesDSL
协议,最后继承到ConstraintDSL
协议,完整的继承关系如下:
1 | public protocol ConstraintDSL { |
这协议都通过extension
定义了一些默认的方法实现首先ConstraintDSL
实现如下:
1 | extension ConstraintDSL { |
这两个方法会为我们调用snp
的view
添加一个标签,用于帮助我们调试,用官方例子来说,就是可以像下面这样添加一个标签:
1 | box.snp.setLabel("box view") //添加用于调试的标签信息 |
ConstraintBasicAttributesDSL
和ConstraintAttributesDSL
协议都是定义了我们可以设置的属性比如left
、width
、centerX
等等,不同的在于ConstraintBasicAttributesDSL
协议中提供的属性是低版本就有的一些通用属性,而ConstraintAttributesDSL
提供的是iOS8
新增的属性,比如lastBaseline
、leftMargin
、topMargin
等。部分代码如下:
1 | extension ConstraintBasicAttributesDSL { |
接下来看定义在ConstraintViewDSL
结构体中的makeConstraints
方法:
1 | public func makeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) { |
这个方法接收一个闭包,然后将闭包传给了ConstraintMaker
的静态方法中,那就看看`makeConstraints静态方法做了什么:
1 | // ConstraintMaker中的静态方法 |
这里LayoutConstraintItem
又是一个协议代码像下面这样:
1 | //定义了一个是能被类遵守的协议 |
再来看makeConstraints
第一行代码所调用的代码:
1 | internal static func prepareConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) -> [Constraint] { |
这个方法就是实际调用闭包的地方,首先将我们的View
构造了ConstraintMaker
对象:
1 | internal init(item: LayoutConstraintItem) { |
接着就调用了我们传进来的闭包closure(maker)
,将刚才的ConstraintMaker
传进去了,所以我们才能使用闭包中的maker
设置各种约束。
链式调用的实现
看看ConstraintMaker
代码:
1 | public class ConstraintMaker { |
由于每个属性都返回ConstraintMakerExtendable
对象所以他们可以链式的调用设置属性的值,
1 | // 这个对象用于设置约束的关系 具体实现篇幅关系 省略了 |
由于每个操作都返回ConstraintMakerExtendable
对象,所以链式调用就实现了
1 | internal let description: ConstraintDescription |
并且最后都操作在了description
这个内部属性上,所以每一步的操作组合成了一条完整的约束所需要的信息。
1 | for description in maker.descriptions { |
启用约束
所以经过closure(maker)
这么一句短短的调用,所有的约束信息就都被收集在[ConstraintDescription]
数组中,所以下面的操作就是取出一条条的约束并且设置启用。
1 | for constraint in constraints { |
下面就是Constraint
关于启用约束的代码,简单看一下实现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
36internal 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
}
//我们在闭包中像make.width.height.equalTo(50)这种组合设置的约束在Constraint内部构造系统约束时都会分开构造,所以这里用数组存储
let layoutConstraints = self.layoutConstraints
//外部传递的参数 是否更新以存在的约束 对于makeConstraints方法 传递的是false
if updatingExisting {
//这里先记录View上已经存在的约束信息
var existingLayoutConstraints: [LayoutConstraint] = []
for constraint in item.constraints {
existingLayoutConstraints += constraint.layoutConstraints
}
//循环将要设置的约束信息
for layoutConstraint in layoutConstraints {
//取出第一个重复的约束
let existingLayoutConstraint = existingLayoutConstraints.first { $0 == layoutConstraint }
//如果不为空
guard let updateLayoutConstraint = existingLayoutConstraint else {
fatalError("Updated constraint could not find existing matching constraint to update: \(layoutConstraint)")
}
//就更新这个约束 先判断是否是width,height等没有相对约束的自身约束,取出要更新的属性
let updateLayoutAttribute = (updateLayoutConstraint.secondAttribute == .notAnAttribute) ? updateLayoutConstraint.firstAttribute : updateLayoutConstraint.secondAttribute
//constraintConstantTargetValueFor将得到的属性值转换成对应的格式更新到约束中
updateLayoutConstraint.constant = self.constant.constraintConstantTargetValueFor(layoutAttribute: updateLayoutAttribute)
}
} else {
//如果不需要更新直接启用新设置的约束
NSLayoutConstraint.activate(layoutConstraints)
//加到View持有的Set中保存起来
item.add(constraints: [self])
}
}
makeConstraints
方法的大致流程就分析完了
不同的构造约束的方法区别
SnapKit
提供了三种方法构造约束,从源码的角度看看它们有什么区别吧
1 | //最常用的 上面以及分析过 |
查找公共父视图
这是一段时间以来有名的面试题,在OC平台的Masonry
中有一个方法mas_closestCommonSuperview
实现了这个算法,原因在于iOS8
之前的版本是没有NSLayoutConstraint.activate()
这么简便的设置约束的方法的,有些约束需要设置在自身上,有些需要设置在公共父视图上,所以Masonry
实现了这个算法,由于Swift
iOS8
以上才可以使用所以就没有必要实现那个算法了。