本文系统介绍 iOS/macOS 下的 Auto Layout DSL 库 SnapKit:技术演进、核心原理、应用场景与源码结构,并引用约束求解理论与业界实践。
📋 目录
一、SnapKit 使用详解
1. 框架概述
SnapKit 是面向 Swift 的 Auto Layout DSL(领域特定语言),用于在代码中以声明式、链式语法描述视图的布局约束,替代冗长的 NSLayoutConstraint 手写与 Visual Format 字符串。其设计目标可概括为:
- 可读性:约束意图接近自然语言(如“左边等于父视图左边”“宽度等于父视图一半”)。
- 类型安全与简洁:利用 Swift 的类型与闭包,减少样板代码。
- 可维护性:链式调用便于增删约束、设置优先级与标识,便于调试冲突。
SnapKit 是 Masonry(Objective-C 时代同类型库)在 Swift 生态中的继任者,二者同属 SnapKit 组织 维护,在 GitHub 上均获得大量 Star(SnapKit 约 20k+),被广泛应用于 iOS/macOS 应用的纯代码布局场景 [1]。
2. 历史演进
技术演进可概括为:手写约束 → Visual Format → Masonry(OC DSL)→ SnapKit(Swift DSL),并与 Apple 布局技术的演进并行。
┌─────────────────────────────────────────────────────────────────────────┐
│ 布局方式演进(示意) │
├─────────────────────────────────────────────────────────────────────────┤
│ 1997 2006–2011 2011 2014 2015+ │
│ Cassowary 手写 引入 Masonry SnapKit │
│ 算法论文 NSLayoutConstraint Auto Layout (OC DSL) (Swift DSL) │
│ 发表 (Mac) (iOS 6+) 链式语法 闭包+链式 │
└─────────────────────────────────────────────────────────────────────────┘
| 阶段 | 代表 | 特点 |
|---|---|---|
| 手写约束 | NSLayoutConstraint(item:attribute:relatedBy:toItem:attribute:multiplier:constant:) | 冗长、易出错、难以阅读。 |
| Visual Format | V:|[a]-[b]| 等 | 字符串描述,类型不安全,复杂布局难表达。 |
| Masonry | Objective-C,Block 链式 | 链式 DSL、可读性高,成为 OC 时代事实标准。 |
| SnapKit | Swift,Closure 链式 | 延续 Masonry 思想,利用 Swift 语法与类型,支持 labeled() 等调试与快捷 API。 |
SnapKit 与 Masonry 的对应关系可理解为:同一套“用链式 DSL 描述约束”的设计哲学,从 Objective-C 迁移到 Swift,并针对 Swift 做了 API 与实现上的优化 [2]。
3. 理论基础:Auto Layout 与 Cassowary
SnapKit 的约束最终仍通过 Auto Layout 交给系统布局引擎执行。Auto Layout 的数学基础是 Cassowary 约束求解算法,理解其思想有助于理解“约束冲突”“优先级”“内在尺寸”等概念。
3.1 Cassowary 算法简述
Cassowary 是一种 增量式线性约束求解算法,基于对偶单纯形法(dual simplex),用于求解由 线性等式与不等式 组成的约束系统 [[3]][[4]]。其特点包括:
- 线性:约束可写成形如 (a_1 x_1 + a_2 x_2 + \cdots = b) 或 (\le/\ge) 的形式,与“视图 A 的左边 = 视图 B 的右边 + 常数”等布局关系一致。
- 增量:可动态增删约束并高效重新求解,适合交互式 UI(窗口缩放、动画中更新约束)。
- 约束层次(constraint hierarchy):支持 required 与 preferred(优先级),在约束冲突时按优先级舍弃或松弛部分约束,避免无解。
参考文献:
- 原始论文:Solving Linear Arithmetic Constraints for User Interface Applications,UIST 1997 [[5]]。
- 扩展与实现:The Cassowary Linear Arithmetic Constraint Solving Algorithm,ACM TOCHI;Washington 大学 Cassowary 工具包 [[6]]。
3.2 从约束描述到线性关系(概念)
Auto Layout 将每条约束映射为关于视图几何变量(如 left, right, width, centerX)的线性等式或不等式。SnapKit 所写的“左边等于父视图左边 + 20”即对应:
- 变量:
view.left、superview.left - 关系:
view.left = superview.left + 20
多约束组成方程组,由 Cassowary 求解得到每个变量的值,从而得到各视图的 frame。优先级 对应 Cassowary 的强弱约束:高优先级必须满足,低优先级在冲突时可被违反。
3.3 流程图:从 SnapKit 到屏幕像素(概念层)
flowchart LR
A[SnapKit API 调用] --> B[ConstraintMaker 等 DSL]
B --> C[Constraint 描述对象]
C --> D[NSLayoutConstraint]
D --> E[Auto Layout 引擎]
E --> F[Cassowary 求解]
F --> G[布局结果 / frame]
G --> H[渲染到屏幕]
4. 核心概念
4.1 约束的组成
在 Auto Layout 中,一条约束可抽象为:
Item1.Attribute1 Relation Item2.Attribute2 * Multiplier + Constant
例如:“视图 A 的右边 = 视图 B 的左边 - 8”即 A.right = B.left - 8。SnapKit 的链式 API 就是对这五元组(Item1, Attribute1, Relation, Item2, Attribute2, Multiplier, Constant)的封装,并增加 优先级(Priority) 与 标识(Identifier) 等元数据。
4.2 优先级与内在尺寸
| 概念 | 说明 |
|---|---|
| 约束优先级 | UILayoutPriority(0–1000),数值越大越优先;系统在冲突时打破低优先级约束。 |
| Content Hugging | “抗拉伸”:视图不愿比其内在内容尺寸更大;优先级高则更易保持紧凑。 |
| Compression Resistance | “抗压缩”:视图不愿比其内在内容尺寸更小;优先级高则更不易被压缩。 |
Label、Button 等有 intrinsicContentSize 的控件依赖 CHCR 与其它约束共同决定最终尺寸;SnapKit 可通过 .contentCompressionResistancePriority / .contentHuggingPriority 等设置(若 API 支持)或直接操作 UIView 的对应属性。
4.3 思维导图:SnapKit 概念关系
mindmap
root((SnapKit))
使用入口
makeConstraints / remakeConstraints / updateConstraints
removeConstraints
描述对象
ConstraintMaker
ConstraintItem
Constraint
约束属性
left right top bottom
width height centerX centerY
edges size margins
关系与修饰
equalTo offset multipliedBy priority
底层
NSLayoutConstraint
Auto Layout / Cassowary
5. API 与使用模式
5.1 基本用法
// 示例:子视图填满父视图边距
view.addSubview(subview)
subview.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
// 等价于四条约束:left/top/right/bottom 分别等于 superview
// 示例:水平居中,宽度为父视图一半,距顶 20
subview.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.width.equalToSuperview().multipliedBy(0.5)
make.top.equalToSuperview().offset(20)
}
5.2 常用 API 对照(伪代码语义)
| SnapKit 写法 | 含义(伪代码) |
|---|---|
make.left.equalToSuperview() | self.left = superview.left |
make.width.equalTo(100) | self.width = 100 |
make.top.equalTo(other.snp.bottom).offset(8) | self.top = other.bottom + 8 |
make.size.equalTo(CGSize(width: 80, height: 80)) | self.width = 80, self.height = 80 |
make.edges.equalToSuperview() | 四边与 superview 对齐 |
make.center.equalToSuperview() | centerX/Y 与 superview 对齐 |
make.width.equalToSuperview().multipliedBy(0.5) | self.width = superview.width * 0.5 |
make.priority(.high) 或 .priority(750) | 为该条约束设置优先级 |
5.3 make / remake / update
- makeConstraints:在已有约束基础上追加新约束,不删除旧约束。
- remakeConstraints:先移除该视图上由 SnapKit 管理的约束,再按闭包重新添加,适合布局整体变化。
- updateConstraints:仅更新闭包中涉及到的约束的 constant(或部分属性),不改变约束条数或关系,适合仅改“间距/常量”的动画或响应式布局。
// 伪代码:remake 的语义
func remakeConstraints(_ closure: (ConstraintMaker) -> Void) {
removeSnapKitConstraints()
makeConstraints(closure)
}
5.4 SnapKit 与 Masonry 对照
| 维度 | Masonry(OC) | SnapKit(Swift) |
|---|---|---|
| 语法载体 | Block ^(MASConstraintMaker *make){} | Closure { make in } |
| 链式返回 | 返回 MASConstraint 等 | 返回 ConstraintMakerExtendable 等 |
| 多属性快捷 | edges、size 等 | edges、size、margins 等 |
| 调试 | 无内置标识 | labeled("xxx") 设置 constraint identifier |
| 维护 | SnapKit 组织,OC 项目常用 | SnapKit 组织,Swift 项目主流 |
6. 应用场景与最佳实践
| 场景 | 建议 |
|---|---|
| 纯代码 UI | 用 SnapKit 替代手写 NSLayoutConstraint,可读性和维护性更好。 |
| 动态布局 | 用 remakeConstraints 或 updateConstraints 配合 UIView.animate 更新 constant,实现动画。 |
| 列表 Cell | 在 prepareForReuse 中避免重复添加约束,可 remake 或复用约束并只更新 constant。 |
| 多分辨率/多设备 | 用 multipliedBy、比例、优先级与 CHCR 适配不同宽度与安全区域。 |
| 约束冲突调试 | 使用 labeled() 为约束设置 identifier,便于在控制台或 Xcode 中识别。 |
7. 使用案例详解
以下案例覆盖常见 UI 场景,便于直接套用或改编。
7.1 单视图:居中与尺寸
// 场景:一个头像视图,居中显示,固定 80x80
let avatarView = UIImageView()
view.addSubview(avatarView)
avatarView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(CGSize(width: 80, height: 80))
}
// 场景:宽度为父视图 60%,高度 44,水平居中,距顶 100
let button = UIButton(type: .system)
view.addSubview(button)
button.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.top.equalToSuperview().offset(100)
make.width.equalToSuperview().multipliedBy(0.6)
make.height.equalTo(44)
}
7.2 多视图垂直/水平排列
// 场景:标题 + 副标题垂直排列,整体居中,间距 8
let titleLabel = UILabel()
let subtitleLabel = UILabel()
view.addSubview(titleLabel)
view.addSubview(subtitleLabel)
titleLabel.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.top.equalToSuperview().offset(60)
}
subtitleLabel.snp.makeConstraints { make in
make.centerX.equalTo(titleLabel)
make.top.equalTo(titleLabel.snp.bottom).offset(8)
}
// 场景:三个等宽按钮水平排列,填满父视图左右边距,间距 12
let leftBtn = UIButton()
let midBtn = UIButton()
let rightBtn = UIButton()
[leftBtn, midBtn, rightBtn].forEach { view.addSubview($0) }
leftBtn.snp.makeConstraints { make in
make.left.equalToSuperview().offset(16)
make.centerY.equalToSuperview()
make.height.equalTo(44)
}
midBtn.snp.makeConstraints { make in
make.left.equalTo(leftBtn.snp.right).offset(12)
make.centerY.equalTo(leftBtn)
make.width.height.equalTo(leftBtn)
}
rightBtn.snp.makeConstraints { make in
make.left.equalTo(midBtn.snp.right).offset(12)
make.right.equalToSuperview().offset(-16)
make.centerY.equalTo(midBtn)
make.width.height.equalTo(midBtn)
}
7.3 安全区域与边距
// 场景:内容贴安全区域,四边留 16pt
let contentView = UIView()
view.addSubview(contentView)
contentView.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide.snp.top).offset(16)
make.left.equalTo(view.safeAreaLayoutGuide.snp.left).offset(16)
make.right.equalTo(view.safeAreaLayoutGuide.snp.right).offset(-16)
make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom).offset(-16)
}
// 使用 edges 的等价写法(SnapKit 对 safeArea 的封装)
contentView.snp.makeConstraints { make in
make.edges.equalTo(view.safeAreaLayoutGuide).inset(UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16))
}
7.4 卡片式布局(内边距 + 圆角容器)
// 场景:卡片内有一个标题和一段正文,整体有内边距
let card = UIView()
let titleLabel = UILabel()
let bodyLabel = UILabel()
card.addSubview(titleLabel)
card.addSubview(bodyLabel)
view.addSubview(card)
card.snp.makeConstraints { make in
make.left.right.equalToSuperview().inset(20)
make.top.equalToSuperview().offset(100)
// 高度由内容撑起,不写 bottom,由子视图约束反推
}
titleLabel.snp.makeConstraints { make in
make.top.left.right.equalToSuperview().inset(16)
}
bodyLabel.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(8)
make.left.right.equalToSuperview().inset(16)
make.bottom.equalToSuperview().offset(-16) // 决定 card 的底部
}
7.5 UIScrollView 内容布局
// 场景:ScrollView 内纵向堆叠内容,可滚动
let scrollView = UIScrollView()
let contentView = UIView()
scrollView.addSubview(contentView)
view.addSubview(scrollView)
scrollView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
contentView.snp.makeConstraints { make in
make.edges.equalToSuperview()
make.width.equalTo(scrollView) // 宽度与 scrollView 一致,避免横向滚动
// 高度由子视图约束决定,最后子视图的 bottom 约束到 contentView.bottom
}
// 在 contentView 内继续添加子视图,最后一个子视图的 bottom 约束到 contentView
let lastView = UIView()
contentView.addSubview(lastView)
lastView.snp.makeConstraints { make in
make.left.right.top.equalToSuperview()
make.height.equalTo(200)
make.bottom.equalToSuperview().offset(-20) // 关键:撑开 contentView 高度
}
7.6 TableView Cell 内约束
// 在 UITableViewCell 子类中
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(titleLabel)
contentView.addSubview(iconView)
titleLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(16)
make.centerY.equalToSuperview()
make.right.lessThanOrEqualTo(iconView.snp.left).offset(-8)
}
iconView.snp.makeConstraints { make in
make.right.equalToSuperview().offset(-16)
make.centerY.equalToSuperview()
make.size.equalTo(CGSize(width: 24, height: 24))
}
}
override func prepareForReuse() {
super.prepareForReuse()
// 不要在这里再次 makeConstraints,否则会重复添加;若需更新内容用 updateConstraints 或只改 constant
}
7.7 动态布局:remake 与 update
// 场景:根据状态切换“展开/收起”,用 remake 重做约束
func setExpanded(_ expanded: Bool) {
contentView.snp.remakeConstraints { make in
make.left.right.top.equalToSuperview()
if expanded {
make.height.equalTo(200)
} else {
make.height.equalTo(60)
}
}
UIView.animate(withDuration: 0.3) {
self.layoutIfNeeded()
}
}
// 场景:只改间距,用 update 更新 constant,适合动画
var topOffset: Constraint?
view.snp.makeConstraints { make in
topOffset = make.top.equalToSuperview().offset(20).constraint
make.left.right.equalToSuperview()
make.height.equalTo(100)
}
// 后续
topOffset?.update(offset: 80)
UIView.animate(withDuration: 0.25) {
view.superview?.layoutIfNeeded()
}
7.8 优先级与可选的“最大宽度”
// 场景:标签最大宽度为父视图 70%,但若内容更短则保持 intrinsic 宽度
label.snp.makeConstraints { make in
make.left.equalToSuperview().offset(16)
make.centerY.equalToSuperview()
make.width.lessThanOrEqualToSuperview().multipliedBy(0.7).priority(.high)
// 不写 right,由 CHCR 与 lessThanOrEqualTo 共同决定
}
7.9 约束标识与调试
view.snp.makeConstraints { make in
make.top.equalToSuperview().offset(20).labeled("headerTop")
make.left.right.equalToSuperview().labeled("headerHorizontal")
}
// 约束冲突时,在控制台或 Xcode 中可根据 "headerTop" 等快速定位视图与约束
7.10 小结表
| 场景 | 推荐 API | 要点 |
|---|---|---|
| 单视图居中/尺寸 | makeConstraints + center / size / equalToSuperview() | 先 addSubview 再约束 |
| 多视图排列 | 先定一个基准视图,其余 equalTo(基准.snp.xxx) | 注意谁决定总高度/总宽度 |
| 安全区域 | view.safeAreaLayoutGuide.snp.xxx 或 edges.equalTo(safeArea).inset() | 适配刘海与 Home Indicator |
| 卡片/内边距 | 父视图不设高度,子视图 bottom.equalToSuperview() 撑开 | 避免固定高度,利于多行文本 |
| ScrollView | contentView.width.equalTo(scrollView) + 最底部子视图 bottom 约束到 contentView | 撑开 contentSize |
| Cell | makeConstraints 在 init 中只做一次,prepareForReuse 不重复添加 | 可配合 remake 或只更新 constant |
| 动态/动画 | remakeConstraints 整体重做,updateConstraints 只改 constant | 动画前改约束,动画内 layoutIfNeeded() |
| 优先级/可选约束 | .priority(.high)、lessThanOrEqualTo 等 | 与 CHCR 配合,避免冲突 |
二、SnapKit 源码解析
1. 整体架构
SnapKit 的代码结构可分层为:DSL 入口层 → 约束描述层 → 约束实体层 → 系统桥接层。
flowchart TB
subgraph 入口
A[View.snp.makeConstraints]
end
subgraph DSL
B[ConstraintMaker]
C[ConstraintDescription]
D[ConstraintItem]
end
subgraph 约束实体
E[Constraint]
F[ConstraintViewAttributes]
end
subgraph 系统
G[NSLayoutConstraint]
H[LayoutConstraint]
end
A --> B
B --> C
C --> D
C --> E
E --> F
E --> G
G --> H
- 入口:
View.snp返回ConstraintViewDSL,其上提供makeConstraints/remakeConstraints/updateConstraints等方法,接收(ConstraintMaker) -> Void闭包。 - ConstraintMaker:闭包中的
make对象,持有当前视图(ConstraintItem)及一组 ConstraintDescription;每次调用make.left.equalTo(...)等会生成或更新一条 ConstraintDescription。 - ConstraintDescription:描述“某属性 与 某目标 的 关系、倍数、常量、优先级”,可生成多条 Constraint(例如
edges生成四条)。 - Constraint:封装最终要安装的 NSLayoutConstraint(或其子类),负责
install()/uninstall()与状态管理。
1.1 关键类型与职责(对照源码)
| 类型 | 文件/模块 | 职责简述 |
|---|---|---|
| ConstraintViewDSL | View+DSL | 通过 view.snp 暴露,提供 makeConstraints / remakeConstraints / updateConstraints,持有 view。 |
| ConstraintMaker | ConstraintMaker | 闭包参数 make,持有 ConstraintItem(当前视图)和 [ConstraintDescription],提供 left/right/top/bottom 等入口。 |
| ConstraintDescription | ConstraintDescription | 描述单条或多条约束(如 edges 对应 4 条),持有 relation、target、multiplier、constant、priority,可生成 Constraint。 |
| ConstraintItem | ConstraintItem | 对 UIView/ UILayoutGuide 的抽象,提供 layoutConstraintItem(用于 NSLayoutConstraint 的 firstItem/secondItem)。 |
| Constraint | Constraint | 对应一条 NSLayoutConstraint,实现 install() 时创建并激活,uninstall() 时 deactivate 并置空引用。 |
| LayoutConstraint | LayoutConstraint | NSLayoutConstraint 子类,用于在 install 时做兼容或扩展(如与 SnapKit 的关联标记)。 |
1.2 约束的生命周期(创建 → 安装 → 更新/移除)
makeConstraints { make in
make.left.equalToSuperview().offset(20) // 1. 生成 ConstraintDescription,加入 maker
}
// 2. 闭包返回后,maker 将 description 转为 Constraint,再对每个 Constraint 调用 install()
// 3. install() 内部:new NSLayoutConstraint(...); constraint.isActive = true
// 4. 若后续调用 remakeConstraints,先 uninstall 所有已安装的 Constraint,再重新执行闭包并 install
2. DSL 链与构建器模式
SnapKit 的链式 API 采用 流式接口(Fluent Interface) 与 构建器思想:每次调用返回可继续链式调用的对象,逐步补全“属性、关系、目标、倍数、常量、优先级”。
典型调用链在概念上可拆成:
make.left → 选定“左边界”为当前约束属性
.equalTo(superview) → 关系为 equal,目标为 superview(默认同属性 left)
.offset(20) → constant = 20
.priority(.high) → 优先级
对应到源码中的角色(名称可能随版本略有差异):
| 类型/协议 | 作用 |
|---|---|
| ConstraintMaker | 入口,提供 left/right/top/bottom/width/height/centerX/centerY/edges/size 等,返回可继续链式的对象。 |
| ConstraintMakerExtendable | 扩展 edges、size、margins 等组合属性。 |
| ConstraintMakerRelatable | 提供 equalTo、lessThanOrEqualTo、greaterThanOrEqualTo,确定“关系 + 目标”。 |
| ConstraintMakerEditable | 提供 offset、multipliedBy、dividedBy 等,设置 constant 与 multiplier。 |
| ConstraintMakerPriortizable | 提供 priority(...),设置约束优先级。 |
| ConstraintMakerFinalizable | 结束链,可能返回 Constraint 供后续引用或批量操作。 |
因此,像 make.width.equalToSuperview().dividedBy(2).priority(100) 的调用会依次经过:选定属性 → 设关系与目标 → 设倍数 → 设优先级,最终生成一条 Constraint 描述并加入 Maker 的列表,在闭包结束后统一 install。
2.1 链式调用的返回类型(协议串联)
链的每一步返回不同协议类型,使下一句只能调用合法方法,形成“约束描述”的逐步补全:
make.width → ConstraintMakerExtendable (可继续 .equalTo / .lessThanOrEqualTo 等)
.equalToSuperview() → ConstraintMakerEditable (可继续 .offset / .multipliedBy 等)
.dividedBy(2) → ConstraintMakerPriortizable (可继续 .priority)
.priority(100) → ConstraintMakerFinalizable (可 .constraint 取引用或结束)
源码中通过协议 + 泛型实现:例如 ConstraintMakerExtendable 的 equalTo(_:) 返回 ConstraintMakerEditable,这样就不能在未设置目标前写 offset,保证调用顺序正确。
2.2 组合属性(edges / size / center)的展开
当写 make.edges.equalToSuperview() 时,内部会展开为四条约束描述:
make.left.equalToSuperview()make.right.equalToSuperview()make.top.equalToSuperview()make.bottom.equalToSuperview()
每条仍走完整的链(equalTo → offset → priority),最终得到 4 个 Constraint 对象。size、center 同理,分别对应 2 条约束。因此一个 ConstraintDescription 可以对应多个 Constraint,在 collect 阶段会全部加入 Maker 的列表,在 install 阶段逐一安装。
3. 约束的生成与安装
3.1 安装流程(泳道图)
sequenceDiagram
participant U as 开发者
participant V as View.snp
participant M as ConstraintMaker
participant C as Constraint
participant S as 系统 Auto Layout
U->>V: makeConstraints { make in ... }
V->>M: 创建 Maker(view)
V->>M: 执行闭包(make)
loop 每条约束描述
U->>M: make.xxx.equalTo(...).offset(...)
M->>M: 添加 ConstraintDescription
end
M->>C: 生成 Constraint 并 collect
V->>C: install()
loop 每条 Constraint
C->>S: 创建/激活 NSLayoutConstraint
end
S-->>V: 布局更新
3.2 算法说明(约束收集与安装)
约束安装可简化为两阶段:
- 收集阶段:闭包执行过程中,不立即创建
NSLayoutConstraint,而是将“视图、属性、关系、目标、multiplier、constant、priority”存入 ConstraintDescription,再在适当时机(如访问constraint或闭包结束)生成 Constraint 对象并加入列表。 - 安装阶段:对列表中每个 Constraint 调用
install(),其内部根据描述创建NSLayoutConstraint,并调用isActive = true(或旧版addConstraint)将约束加入视图层级,由系统布局引擎求解。
伪代码(安装逻辑概念):
function Constraint.install():
if alreadyInstalled then return
let c = NSLayoutConstraint(
item: self.view, attribute: self.attr,
relatedBy: self.relation,
toItem: self.targetView, attribute: self.targetAttr,
multiplier: self.multiplier, constant: self.constant
)
c.priority = self.priority
c.isActive = true
self.layoutConstraint = c
mark as installed
3.3 ConstraintDescription → Constraint 的生成时机
- 单属性(如
make.width.equalTo(100)):在链结束(闭包内该句执行完)时,Maker 根据当前 Description 生成一个 Constraint,加入内部数组;若链上有.constraint,则同时返回给调用方保存。 - 组合属性(如
make.edges.equalToSuperview()):一条 Description 会展开成多个 Constraint(edges → 4 个),全部加入数组。 - 闭包整体结束:Maker 的
install()被调用,遍历所有已收集的 Constraint,依次执行各自的install(),此时才创建并激活NSLayoutConstraint。
因此“生成 Constraint”与“安装到系统”是分离的:先收集、后统一安装,便于支持 remake(先 uninstall 再重新 make)和批量操作。
3.4 uninstall 与 remake 的配合
remakeConstraints 的语义等价于:
1. 取出该 view 上由 SnapKit 管理的所有 Constraint(通过关联对象或 view 上的标记)
2. 对每个 Constraint 调用 uninstall():layoutConstraint.isActive = false,并清空对 NSLayoutConstraint 的引用
3. 再执行 makeConstraints(closure),重新收集并 install
这样可避免旧约束残留导致的冲突或多余约束。
4. 与系统 Auto Layout 的衔接
SnapKit 不实现自己的布局引擎,而是 生成并激活 NSLayoutConstraint,完全依赖系统 Auto Layout(及底层 Cassowary 求解器)。因此:
- 性能:约束求解与布局计算由系统完成,SnapKit 只影响“约束的创建与组织方式”。
- 兼容性:与 Interface Builder、手写约束、其他第三方布局库生成的约束可混用,只要约束系统一致(无冲突或冲突可被优先级解决)。
- 调试:约束冲突、无法满足的约束等仍由系统报错;SnapKit 的
labeled()可为生成的NSLayoutConstraint.identifier赋值,便于在 Xcode 中识别。
4.1 SnapKit 属性与 NSLayoutConstraint.Attribute 的对应
SnapKit 的 ConstraintAttribute(left, right, top, bottom, width, height, centerX, centerY 等)在 install 时会被映射为系统的 NSLayoutConstraint.Attribute:
| SnapKit 概念 | NSLayoutConstraint.Attribute |
|---|---|
| left | .left |
| right | .right |
| top | .top |
| bottom | .bottom |
| leading | .leading |
| trailing | .trailing |
| width | .width |
| height | .height |
| centerX | .centerX |
| centerY | .centerY |
| leftMargin / rightMargin 等 | .leftMargin, .rightMargin, ... |
multiplier、constant、relation、priority 则直接传给 NSLayoutConstraint 的对应参数;toItem 与 secondAttribute 来自 ConstraintDescription 的 target(ConstraintItem),若目标为常数(如 equalTo(100)),则 toItem 为 nil,secondAttribute 为 .notAnAttribute。
5. 关键数据结构与约束映射
5.1 ConstraintItem:视图与 LayoutGuide 的统一抽象
系统 API 中,约束的 firstItem / secondItem 可以是 UIView 或 UILayoutGuide。SnapKit 用 ConstraintItem 封装二者,对外只暴露“某个对象 + 其 snp 描述”,这样 equalToSuperview() 与 equalTo(view.safeAreaLayoutGuide.snp.top) 可以走同一套链式 API。内部在生成 NSLayoutConstraint 时,从 ConstraintItem 取出真正的 layoutConstraintItem(UIView 或 UILayoutGuide)作为 firstItem/secondItem。
5.2 约束描述到 NSLayoutConstraint 的构造(概念代码)
一条 Constraint 在 install() 时大致等价于:
// 概念代码,非逐字源码
func install() {
guard layoutConstraint == nil else { return }
let firstItem = description.view.layoutConstraintItem!
let secondItem = description.target?.layoutConstraintItem // 可为 nil
let c = NSLayoutConstraint(
item: firstItem,
attribute: description.attribute.layoutAttribute,
relatedBy: description.relation,
toItem: secondItem,
attribute: secondItem != nil ? description.targetAttribute.layoutAttribute : .notAnAttribute,
multiplier: description.multiplier,
constant: description.constant
)
c.priority = description.priority
c.identifier = description.label
c.isActive = true
self.layoutConstraint = c
}
理解这一点即可知道:SnapKit 不参与求解,只负责“描述 → NSLayoutConstraint → isActive = true”,布局结果完全由系统 Auto Layout(及 Cassowary)决定。
5.3 updateConstraints 的“只改 constant”实现
updateConstraints 与 makeConstraints 共用同一套收集逻辑,但语义是“更新已存在的约束”。实现上通常通过约束匹配:根据“视图 + 属性”(以及可选的 target)找到之前由 SnapKit 安装的 Constraint,只调用其 update(offset:) / update(inset:) 等,修改底层 NSLayoutConstraint.constant,而不新增或删除约束。因此适合“布局关系不变、只改间距或尺寸常量”的动画或响应式更新。
三、设计模式与延伸
与 Masonry 一脉相承,SnapKit 在架构中同样运用了多种设计模式与编程思想;因采用 Swift 与协议导向设计,部分实现方式与 Masonry(OC)不同,但目标一致:可读的链式 DSL、统一的约束抽象、先描述后安装。下表对照 SnapKit 与 Masonry,便于与《05-Masonry框架:从使用到源码解析》对照学习。
| 模式/技巧 | 在 SnapKit 中的体现 | 与 Masonry 对照 |
|---|---|---|
| 组合思想 | 单条约束(如 make.left)与复合约束(如 make.edges)对外同一套链式 API;edges / size / center 在内部展开为多条 Constraint,统一通过 ConstraintMaker 收集、再逐一 install。无显式 Composite 类,但“单条与复合同一接口”的思想一致。 | Masonry 用 MASConstraint 协议 + MASViewConstraint(叶子)+ MASCompositeConstraint(组合)形成约束树;SnapKit 用协议链 + 一条 Description 对应多条 Constraint 实现类似效果。 |
| 工厂/构建器思想 | ConstraintMaker 根据访问的属性(left、width、edges…)创建或填充 ConstraintDescription,调用方不直接 Constraint(...);闭包内“描述”、闭包外统一 install,符合“构建器 + 两阶段”的模式。 | Masonry 的 MASConstraintMaker 按属性创建 MASViewConstraint / MASCompositeConstraint,形态上更接近简单工厂;SnapKit 的 Maker 更突出“分步填写再构建”的构建器角色。 |
| 链式/流式接口 | 每一步返回不同协议类型(ConstraintMakerExtendable → Relatable → Editable → Priortizable → Finalizable),既形成链式调用,又用类型约束“先设目标再设 offset/priority”,避免错误顺序。 | Masonry 用 Block 属性 getter 返回“带返回值的 Block”,Block 内 return self 形成链;SnapKit 用 Swift 协议与泛型在编译期保证链的顺序。 |
| 类型安全与多态入口 | Swift 泛型与重载实现 equalTo(100)、equalTo(CGSize)、equalTo(view) 等统一入口,无需 OC 的“装箱”;编译器区分类型,无运行时 BoxValue。 | Masonry 用 MASBoxValue 将标量/结构体装箱为 id,再走 equalTo:;SnapKit 用语言特性替代,思想一致(统一入口、多类型支持)。 |
| 两阶段处理 | 闭包内只向 Maker 追加 ConstraintDescription / Constraint,不立即创建 NSLayoutConstraint;闭包结束后再统一 install,便于 remake(先 uninstall 再 make)与批量操作。 | 与 Masonry 的“block(maker) 只登记,[maker install] 再创建并激活”完全一致。 |
提炼与串联:上述模式与思想在 SnapKit 中的协作关系、与 Masonry 的异同,以及可复用要点,见 §六、编程思想与设计模式提炼总结。更详细的模式定义与伪代码可参考本系列《05-Masonry框架:从使用到源码解析》中的“简单工厂 | 工厂方法 | 抽象工厂”“组合模式与约束树”“链式语法完整解析”等小节。
四、SnapKit 中的优秀编程思想
SnapKit 能成为 iOS 布局 DSL 的事实标准,不仅因为功能完善,更因为其背后一系列可复用的编程思想。理解这些思想有助于在业务代码或自研库中写出更易读、可维护的 API。
1. DSL(领域特定语言):用“布局语言”说话
思想:不暴露通用编程语言的细枝末节,而是提供一套贴近领域(这里是“布局约束”)的词汇和语法,让代码读起来像在描述布局本身。
对比:系统 API 是“给引擎传参数”;SnapKit 是“用布局语言写句子”。
// 系统 API:面向“约束引擎”,不直观
NSLayoutConstraint(
item: subview,
attribute: .left,
relatedBy: .equal,
toItem: superview,
attribute: .left,
multiplier: 1,
constant: 20
)
// SnapKit:面向“布局意图”,读即懂
subview.snp.makeConstraints { make in
make.left.equalToSuperview().offset(20)
}
可复用的点:在业务里遇到“一坨参数、含义不清”的 API 时,可以封装一层 DSL:用类型 + 闭包 + 链式方法,把“做什么”说清楚,把“怎么做”藏进实现。
2. 流式接口(Fluent Interface):链式调用表达顺序
思想:每一步方法返回“可继续操作”的对象,让多步操作写成一串链,顺序即逻辑,无需临时变量。
代码示例:
// 链式:属性 → 关系与目标 → 常量/倍数 → 优先级,一气呵成
label.snp.makeConstraints { make in
make.top.equalToSuperview().offset(16)
.labeled("titleTop") // 链上可继续加修饰
make.left.equalToSuperview().offset(20)
make.right.lessThanOrEqualToSuperview().offset(-20)
.priority(.high)
}
设计要点:返回值类型随链变化(如 equalTo 后返回支持 offset 的类型),编译器保证“先设目标再设 offset”,避免错误顺序。这种思想在构建查询、配置对象时同样适用。
3. 构建器模式(Builder):分步构建复杂对象
思想:约束是一个“复杂对象”(属性、关系、目标、倍数、常量、优先级)。不一次性传 7 个参数,而是用多个小方法分步填写,最后统一“安装”。
代码示例:
// 构建器:先描述,后安装
view.snp.makeConstraints { make in
// 步骤 1:选属性
make.width
// 步骤 2:设关系与目标
.equalToSuperview()
// 步骤 3:设倍数与常量
.multipliedBy(0.5)
.offset(0)
// 步骤 4:设优先级(可选)
.priority(.medium)
}
// 闭包结束后统一 install,而非每写一句就加一条系统约束
可复用的点:任何“多参数、多可选、有顺序”的配置,都可以用 Builder:一个入口方法接收闭包,闭包里对“builder 对象”调用多个 setter,最后在闭包外统一执行(如网络请求的 Builder、配置文件的 Builder)。
3.5 组合模式统一接口:单条与复合用同一套 API(对照 Masonry)
思想:调用方不区分“单条约束”还是“多条约束的集合”,都通过同一套链式 API 操作;复合约束(如 edges、size)在内部展开为多条 Constraint,但对外呈现一致。
在 SnapKit 中的体现:make.left 与 make.edges 都返回可继续链式的类型(如 ConstraintMakerExtendable / Relatable),都可继续 .equalTo(...).offset(...).priority(...);make.edges.equalToSuperview() 内部会展开为 left/right/top/bottom 四条 Constraint 并加入 Maker,与 Masonry 的 MASCompositeConstraint(edges 对应四条 MASViewConstraint)思想一致。详见 二、2.2 组合属性(edges / size / center)的展开。
与 Masonry 对照:Masonry 用 MASConstraint 协议 + MASViewConstraint(叶子)+ MASCompositeConstraint(组合)形成显式约束树;SnapKit 用“一条 Description 对应多条 Constraint”实现同一语义,无单独 Composite 类,但“单条与复合同一接口”的用法一致。
3.6 两阶段处理:先描述,再安装(对照 Masonry)
思想:闭包执行阶段只“收集意图”,不立刻产生副作用(不立刻创建或激活 NSLayoutConstraint);等闭包结束后再统一 install,便于去重、批量激活、remake(先 uninstall 再 make)。
在 SnapKit 中的体现:view.snp.makeConstraints { make in ... } 中,闭包内 make.xxx 只向 ConstraintMaker 追加 ConstraintDescription 或生成 Constraint 并加入列表;闭包返回后,框架再对列表中每个 Constraint 调用 install(),此时才创建并激活 NSLayoutConstraint。与 Masonry 的“block(maker) 只登记,[maker install] 再创建并激活”完全一致。详见 二、3. 约束的生成与安装。
4. 类型安全与协议拆分:用类型约束“能写什么”
思想:通过协议 + 泛型把“当前能调用的方法”限定在类型里。例如:只有调用了 equalTo 之后才允许调用 offset;只有调用了 offset 之后才允许调用 priority。这样错误顺序在编译期就会报错。
概念示例(对应 SnapKit 的协议链):
// 伪代码:协议链保证“先选目标再设常量”
protocol ConstraintMakerExtendable {
var left: ConstraintMakerRelatable { get }
var width: ConstraintMakerRelatable { get }
}
protocol ConstraintMakerRelatable {
func equalTo(_ other: ConstraintItem) -> ConstraintMakerEditable
func equalTo(_ constant: CGFloat) -> ConstraintMakerEditable
}
protocol ConstraintMakerEditable {
func offset(_ c: CGFloat) -> ConstraintMakerPriortizable
func multipliedBy(_ m: CGFloat) -> ConstraintMakerPriortizable
}
protocol ConstraintMakerPriortizable {
func priority(_ p: UILayoutPriority) -> ConstraintMakerFinalizable
}
// 因此:make.left.offset(20) 会编译错误,因为 left 之后必须先 equalTo
业务中的用法:例如“配置请求”时,可以设计成:只有设置了 URL 才能设置 Method,只有设置了 Method 才能设置 Body,避免漏配或顺序错乱。
5. 闭包与延迟执行:描述与执行分离
思想:约束的描述(闭包内的 make.xxx)和执行(真正创建并激活 NSLayoutConstraint)分离。闭包负责“声明要什么”,框架在闭包返回后统一“收集、生成、安装”。
代码示例:
// 闭包内只“描述”,不立刻生效
view.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
// 这里才真正 install;若内部用 remake,会先 uninstall 再根据闭包重新 install
好处:可以统一做“去重、校验、批量安装、与旧约束对比”等逻辑,而不让调用方关心。在其他场景里,例如“先收集所有配置再一次性提交”“先构建命令再执行”,也适合“闭包描述 + 闭包外执行”的模式。
6. 单一职责与分层:谁只做一件事
思想:
- ConstraintMaker:只负责“收集约束描述”。
- ConstraintDescription:只负责“一条/多条约束的参数”。
- Constraint:只负责“对应一条 NSLayoutConstraint 的安装/卸载”。
- View + snp:只负责“入口和闭包调度”。
每一层只做一件事,便于测试和替换;例如以后要支持“约束预览”或“导出为 IB 约束”,只需在描述层或安装层加一层,而不必改 DSL 写法。
代码层面的体现:
- 改 constant 用
Constraint.update(offset:),不碰 Maker。 - 改约束集合用
remakeConstraints,由 Maker 重新收集再安装,Constraint 只负责单条的生命周期。
7. 可读性与“表达意图”:命名即文档
思想:API 命名直接表达意图,而不是实现细节。例如 equalToSuperview() 比 equalTo(view.superview!) 更贴近“与父视图对齐”的意图;labeled("headerTop") 直接表达“方便调试时识别”。
代码示例:
// 意图明确:居中、宽度为父视图一半、距顶 20
avatar.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.width.equalToSuperview().multipliedBy(0.5)
make.top.equalToSuperview().offset(20)
}
// 意图明确:四边与安全区域对齐并留内边距
content.snp.makeConstraints { make in
make.edges.equalTo(view.safeAreaLayoutGuide).inset(16)
}
可复用的点:对外 API 尽量用“业务/领域术语”命名(如 equalToSuperview、inset),内部实现可以用技术术语(如 layoutConstraintItem、constant),让调用方代码即文档。
8. 小结:思想与可复用场景
| 编程思想 | SnapKit 中的体现 | 可复用场景举例 |
|---|---|---|
| DSL | 布局专用词汇与语法 | 配置、查询、脚本类 API |
| 流式接口 | 链式 make.xxx.equalTo().offset() | 配置对象、查询构建器 |
| 构建器模式 | 分步填约束再统一 install | 多参数配置、请求/命令构建 |
| 组合模式统一接口 | 单条(make.left)与复合(make.edges)同一套 API,内部展开多条 Constraint | 树形结构、批量操作、配置项分组 |
| 两阶段处理 | 闭包内只描述,闭包外统一 install | 批量提交、事务、布局、表单校验 |
| 类型安全与协议拆分 | 不同链阶段返回不同协议 | 有顺序的配置、状态机式 API |
| 闭包 + 延迟执行 | 闭包内描述,闭包外安装 | 批量提交、事务式操作 |
| 单一职责与分层 | Maker / Description / Constraint 各管一事 | 任何多步骤的领域逻辑 |
| 表达意图的命名 | equalToSuperview、labeled、inset | 所有对外 API 设计 |
五、高级应用与注意点
5.1 动画中更新约束
// 仅更新 constant,不增删约束
view.snp.updateConstraints { make in
make.top.equalToSuperview().offset(newOffset)
}
UIView.animate(withDuration: 0.3) {
view.superview?.layoutIfNeeded()
}
5.2 约束的引用与批量操作
部分场景需要保留对某条约束的引用(例如单独改 constant 或 priority),SnapKit 支持在闭包中返回或捕获约束:
var widthConstraint: Constraint?
view.snp.makeConstraints { make in
widthConstraint = make.width.equalTo(100).constraint
}
// 后续可修改
widthConstraint?.update(offset: 200)
5.3 安全区域与可读区域
在 iOS 11+ 中,应结合 safeAreaLayoutGuide 做刘海与 Home Indicator 适配;SnapKit 通过 make.top.equalTo(view.safeAreaLayoutGuide.snp.top) 或封装好的安全区 API(视版本而定)与系统安全区对齐,避免内容被遮挡。
六、编程思想与设计模式提炼总结
本节对 SnapKit 中使用的设计模式与编程思想做统一提炼,并与 Masonry 做简要对照,便于在其它 DSL、配置类 API 或自研框架中复用。更详细的模式定义、伪代码与“按目标选模式”清单可参考本系列《05-Masonry框架:从使用到源码解析》中的 五、编程思想与设计模式提炼总结。
6.1 思维导图:SnapKit 设计模式与编程思想总览
mindmap
root((SnapKit 思想与模式))
设计模式
组合思想
单条与复合同一 API
edges/size 展开为多条 Constraint
工厂/构建器思想
ConstraintMaker 按属性创建 Description
闭包内描述 闭包外 install
链式/流式接口
协议链 ConstraintMakerExtendable → Editable → Priortizable
每步返回可继续链的类型
编程思想
DSL
布局词汇 left equalTo offset
代码即文档
两阶段处理
阶段一 闭包内收集描述
阶段二 闭包外统一 install
类型安全
泛型与重载 equalTo(CGFloat)/equalTo(View)
无需装箱 编译期区分
单一职责与分层
Maker / Description / Constraint 各管一事
与 Masonry 对照
同一套“链式 DSL + 两阶段”哲学
Swift 协议链 vs OC Block 返回 self
无显式 Composite 类 语义一致
6.2 设计模式与编程思想提炼表(与 Masonry 对照)
| 模式/思想 | SnapKit 中的体现 | Masonry 对照 |
|---|---|---|
| 组合思想 | 单条与复合(edges/size)同一套链式 API;一条 Description 可对应多条 Constraint。 | MASConstraint 协议 + MASViewConstraint(叶子)+ MASCompositeConstraint(组合)形成约束树。 |
| 工厂/构建器 | ConstraintMaker 根据访问属性创建/填充 ConstraintDescription;分步填写再统一 install。 | MASConstraintMaker 按属性创建 MASViewConstraint / MASCompositeConstraint(简单工厂形态)。 |
| 流式接口 | 协议链保证每步返回可链类型,编译期约束顺序。 | Block getter 返回“带返回值的 Block”,Block 内 return self。 |
| 两阶段 | 闭包内只描述,闭包外统一 install。 | block(maker) 只登记,[maker install] 再创建并激活。 |
| 类型/多态入口 | Swift 泛型与重载,equalTo(100)/equalTo(CGSize)/equalTo(view)。 | MASBoxValue 装箱,mas_equalTo 宏统一走 equalTo:。 |
6.3 小结:一句话提炼
- 组合:单条与复合同一接口,复合在内部展开为多条 Constraint。
- 构建器:Maker 分步收集描述,闭包外统一 install。
- 流式:协议链每步返回可链类型,顺序即逻辑。
- 两阶段:先描述后执行,便于 remake、批量与扩展。
- 类型安全:Swift 泛型与重载替代 OC 装箱,思想一致。
SnapKit 与 Masonry 在“链式 DSL + 两阶段 + 组合式约束抽象”上保持同一套设计哲学,差异主要来自语言特性(Swift 协议与泛型 vs OC Block 与 id)。理解并提炼后,可在任意“配置型、构建型、DSL 型”的 API 设计中按需复用;与 Masonry 的对照有助于在 OC 与 Swift 项目间迁移或做技术选型。
附录:参考文献与延伸阅读
参考文献
[1] SnapKit. SnapKit. GitHub. github.com/SnapKit/Sna…
[2] Larder. What's in your Larder: iOS layout DSLs. larder.io/blog/larder…
[3] Cassowary. Solving constraint systems. cassowary.readthedocs.io/en/latest/t…
[4] Apple. Auto Layout Guide. Developer Documentation.
[5] Badros, G. J., Borning, A., & Marriott, K. (1997). Solving Linear Arithmetic Constraints for User Interface Applications. Proceedings of the 1997 ACM Symposium on User Interface Software and Technology (UIST).
[6] University of Washington. Cassowary Constraint Solving Toolkit. constraints.cs.washington.edu/cassowary/
[7] Vasarhelyi, A. Behind the Scenes with Auto Layout or How to Solve Constraints with the Cassowary Algorithm. iOSConfSG. speakerdeck.com/vasarhelyia…
延伸阅读
- Masonry:SnapKit 的 Objective-C 前身。本系列《05-Masonry框架:从使用到源码解析》中的三、设计模式与延伸、四、优秀编程思想、五、编程思想与设计模式提炼总结详细展开组合模式、工厂/链式、两阶段、装箱等,与本文 §三、§四、§六 对照可加深对“链式 DSL + 两阶段”设计哲学的理解。
- Auto Layout 内在尺寸:Content Hugging 与 Compression Resistance 在 Apple《Auto Layout Guide》中的说明。
- Cassowary 论文与技术报告:深入理解约束层次与增量求解,便于分析复杂布局冲突与性能。