02-研究优秀开源框架@UI布局@iOS | SnapKit 框架:从使用到源码解析

7 阅读28分钟

本文系统介绍 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 FormatV:|[a]-[b]|字符串描述,类型不安全,复杂布局难表达。
MasonryObjective-C,Block 链式链式 DSL、可读性高,成为 OC 时代事实标准。
SnapKitSwift,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):支持 requiredpreferred(优先级),在约束冲突时按优先级舍弃或松弛部分约束,避免无解。

参考文献

  • 原始论文: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.leftsuperview.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 等
多属性快捷edgessizeedgessizemargins
调试无内置标识labeled("xxx") 设置 constraint identifier
维护SnapKit 组织,OC 项目常用SnapKit 组织,Swift 项目主流

6. 应用场景与最佳实践

场景建议
纯代码 UI用 SnapKit 替代手写 NSLayoutConstraint,可读性和维护性更好。
动态布局remakeConstraintsupdateConstraints 配合 UIView.animate 更新 constant,实现动画。
列表 CellprepareForReuse 中避免重复添加约束,可 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.xxxedges.equalTo(safeArea).inset()适配刘海与 Home Indicator
卡片/内边距父视图不设高度,子视图 bottom.equalToSuperview() 撑开避免固定高度,利于多行文本
ScrollViewcontentView.width.equalTo(scrollView) + 最底部子视图 bottom 约束到 contentView撑开 contentSize
CellmakeConstraints 在 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 关键类型与职责(对照源码)

类型文件/模块职责简述
ConstraintViewDSLView+DSL通过 view.snp 暴露,提供 makeConstraints / remakeConstraints / updateConstraints,持有 view。
ConstraintMakerConstraintMaker闭包参数 make,持有 ConstraintItem(当前视图)和 [ConstraintDescription],提供 left/right/top/bottom 等入口。
ConstraintDescriptionConstraintDescription描述单条或多条约束(如 edges 对应 4 条),持有 relation、target、multiplier、constant、priority,可生成 Constraint。
ConstraintItemConstraintItem对 UIView/ UILayoutGuide 的抽象,提供 layoutConstraintItem(用于 NSLayoutConstraint 的 firstItem/secondItem)。
ConstraintConstraint对应一条 NSLayoutConstraint,实现 install() 时创建并激活,uninstall() 时 deactivate 并置空引用。
LayoutConstraintLayoutConstraintNSLayoutConstraint 子类,用于在 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扩展 edgessizemargins 等组合属性。
ConstraintMakerRelatable提供 equalTolessThanOrEqualTogreaterThanOrEqualTo,确定“关系 + 目标”。
ConstraintMakerEditable提供 offsetmultipliedBydividedBy 等,设置 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 取引用或结束)

源码中通过协议 + 泛型实现:例如 ConstraintMakerExtendableequalTo(_:) 返回 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 对象。sizecenter 同理,分别对应 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 算法说明(约束收集与安装)

约束安装可简化为两阶段:

  1. 收集阶段:闭包执行过程中,不立即创建 NSLayoutConstraint,而是将“视图、属性、关系、目标、multiplier、constant、priority”存入 ConstraintDescription,再在适当时机(如访问 constraint 或闭包结束)生成 Constraint 对象并加入列表。
  2. 安装阶段:对列表中每个 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 的对应参数;toItemsecondAttribute 来自 ConstraintDescription 的 target(ConstraintItem),若目标为常数(如 equalTo(100)),则 toItem 为 nil,secondAttribute.notAnAttribute


5. 关键数据结构与约束映射

5.1 ConstraintItem:视图与 LayoutGuide 的统一抽象

系统 API 中,约束的 firstItem / secondItem 可以是 UIViewUILayoutGuide。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”实现

updateConstraintsmakeConstraints 共用同一套收集逻辑,但语义是“更新已存在的约束”。实现上通常通过约束匹配:根据“视图 + 属性”(以及可选的 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 操作;复合约束(如 edgessize)在内部展开为多条 Constraint,但对外呈现一致。

在 SnapKit 中的体现make.leftmake.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 尽量用“业务/领域术语”命名(如 equalToSuperviewinset),内部实现可以用技术术语(如 layoutConstraintItemconstant),让调用方代码即文档。


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 论文与技术报告:深入理解约束层次与增量求解,便于分析复杂布局冲突与性能。