SnapKit 是怎样炼成的 | 掘金技术征文

1,270 阅读5分钟

前言

这是对 Swift 布局框架 SnapKit 的源码的一点分析,尝试搞清,一个好的布局框架,背后都做了些什么。

介绍 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) -> ConstraintMakerEditableequalToSuperview 的来源。核心方法是 relatedTo(_ other: ConstraintRelatableTarget, relation: ConstraintRelation, file: String, line: UInt) -> ConstraintMakerEditable,返回 ConstraintMakerEditable 类型的实例

ConstraintMakerEditable(继承 ConstraintMakerPriortizable) 在设定约束的宽度、高度以及偏移的时候,提供相应的加减乘除方法,返回 ConstraintMakerPriortizable 类型的实例

ConstraintMakerPriortizable(继承 ConstraintMakerFinalizable) 提供方法来设置约束的 priority,返回 ConstraintMakerFinalizable 类型的实例

ConstraintMakerFinalizable 一个只有一个类型为 ConstraintDescription 的属性的类,正如它的类名,有一个 ConstraintMakerFinalizable 实例,就得到了对于一个约束的完整描述。


至此,我们已经知道 SnapKit 是靠什么来确定了三个东西:

  1. 谁在做约束(ConstraintView)

  2. 怎么做约束(ConstraintMaker)

  3. 约束是什么(ConstraintDescription)

    let aView = UIView() aView.snp.makeConstraints({ make in make.width.equalToSuperview().dividedBy(2).priority(100) }) 当我们写下这样的语句时,先忽略掉 snp 是什么不管,里面设定 aView 的宽度为它的父视图的一半的这行约束语句,执行了这样的逻辑:

  4. ConstraintMaker 提供 makeConstraints 方法来让我们写约束的同时,开始维护了一个 ConstraintDescription 数组,叫 descriptions

  5. make 本身是 ConstraintMaker 类型的

  6. 在我们写下 .width 时,descriptions 数组第一次加入内容(self.description),同时我们用这个内容生成了一个 ConstraintMakerRelatable 实例

  7. 在我们写下 .equalToSuperview() 时,上一步中的内容(self.description)继续添加信息,同时我们用它生成了一个 ConstraintMakerEditable 实例

  8. 之后的 .dividedBy(2).priority(100) 使得之前的 ConstraintMakerEditable 实例变成了一个 ConstraintMakerFinalizable 实例,这个实例的 description 属性的类型是 ConstraintDescription,它包含了我们所描述的全部内容。但由于 ConstraintMakerEditable 本身就继承自 ConstraintMakerFinalizable,所以 .dividedBy(2).priority(100) 这一部分即便不写,这条语句在语法上也已经完成。

image.png

做个总结:到这里我们发现 ConstraintMaker 以及和它相关的类,构造了一套 DSL 来让我们可以轻松地写出约束语句,而这些语句把信息都放到了一个 ConstraintDescription 实例(self.description)里面,但我们仍然不知道它是如何以 UIKit 里面的 NSLayoutConstraint 的形式作用的。

snp 是什么

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 这样的语法来写约束。

这个 snp 属性的类型就是结构体 ConstraintViewDSL

一看就是面向协议的写法,通过一个个的 extension 来给 protocol 添加功能,最后用 struct 实现出来,就有了 snp 这个属性。

let topView = UIView()
let centerView = UIView()
centerView.snp.makeConstraints({ make in
    make.top.equalTo(topView.snp.bottom).offset(16)
})

这段代码展现了 snp 的两个作用:

  1. snp 有 left top right bottom edges size 等一大堆属性,这些属性的类型是 ConstraintItem,这是用于构造约束位置关系的

image.png 2. snp 作为 ConstraintViewDSL,有 prepareConstraints makeConstraints remakeConstraints updateConstraints removeConstraints 等函数,我们最常用的是 makeConstraints ,传入一个 closure,在里面写约束关系。这里要注意,我们使用的 makeConstraints 方法来源于 ConstraintViewDSL,但真正实现了构造约束的其实是我们上文里面写的 ConstraintMaker 里面的 makeConstraints 方法,见图:

image.png

约束是如何作用的

到现在我们还是没说,从 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 数组。

image.png Constraint 这个类还没有介绍过,不过上面这个核心方法加上以前的内容,已经可以让我们猜出来,约束是怎么写出来的了:

image.png

其他内容补充 1

image.png 随便写了两句,展示一下各个方法传入的参数的类型,发现有各种 Target,貌似很复杂,不过点开之后发现是这种景象:

image.png 说白了就是因为 equalTo: 这个方法里面能传的参数类型比较多,手动来一个一个限制一下,我们看到 ConstraintRelatableTarget 这里可以放一些原生的可以代表数字的类型,外加四个自定义的 Constraint 类型。其他的 Target 协议也差不多是这种情况。

个人觉得这种做法还是挺值得学习的。

其他内容补充 2

SnapKit 里面用来表示位置主体的类其实不是 ConstraintView,而是 ConstraintItem 我们管这个“主体”叫 target,一个 target,再加上一个 ConstraintAttributes 实例,就可以组成一个 ConstraintItem。

image.png 有 attributes 属性很好理解,因为比如我们去做对齐,可以是 aView 的 top 和 bView 的 bottom 对齐,而不能是 aView 和 bView 对齐。但是为什么 target 的类型是 AnyObject 而不是 ConstraintView,即 UIView 或 NSView 呢?

在 ConstraintViewDSL 里面,target 确实是 ConstraintView 类型, 但在 ConstraintLayoutSupportDSL 里面,target 是 ConstraintLayoutSupport 类型, 在 ConstraintLayoutGuideDSL 里面,target 是 ConstraintLayoutGuide 类型

这部分就不具体解释了,想一探究竟的去看 LayoutConstraintItem.swift 这个文件吧。

掘金技术征文:gold.xitu.io/post/58522d…