【SnapKit】优雅的 Swift Auto Layout DSL 库
iOS三方库精读 · 第 4 期
一、一句话介绍
SnapKit 是一个用于 iOS/macOS/tvOS 的 Swift Auto Layout DSL 库,它让繁琐的界面约束编写变得简洁优雅,是 UIKit 开发中最受欢迎的布局解决方案之一。
- Stars: 19k+ ⭐
- 最新版本: 5.7.0
- License: MIT
- 支持平台: iOS 12.0+ / macOS 10.13+ / tvOS 12.0+
二、为什么选择它
原生 NSLayoutConstraint 的痛点
在 UIKit 中,使用原生 API 创建约束通常是这样的:
// 原生方式 - 需要 4 行代码创建一个约束
let constraint = NSLayoutConstraint(
item: view,
attribute: .leading,
relatedBy: .equal,
toItem: superview,
attribute: .leading,
multiplier: 1.0,
constant: 16
)
constraint.isActive = true
view.translatesAutoresizingMaskIntoConstraints = false
SnapKit 的核心优势
- 链式 DSL 语法:一行代码表达一个约束意图,代码可读性大幅提升
- 类型安全:编译期检查约束目标,避免运行时因字符串 API 导致的崩溃
- 自动管理:自动设置
translatesAutoresizingMaskIntoConstraints = false - 动态更新:支持
updateConstraints和remakeConstraints,轻松应对动态布局 - 优先级支持:链式设置约束优先级,优雅处理约束冲突
三、核心功能速览
基础层 概念解释、环境配置、基础用法
环境要求与集成
SPM 集成:
// Package.swift
dependencies: [
.package(url: "https://github.com/SnapKit/SnapKit.git", from: "5.7.0")
]
CocoaPods 集成:
pod 'SnapKit', '~> 5.7.0'
最简单的使用示例
import SnapKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let box = UIView()
box.backgroundColor = .systemBlue
view.addSubview(box)
// 使用 SnapKit 创建约束
box.snp.makeConstraints { make in
make.center.equalToSuperview()
make.width.height.equalTo(100)
}
}
}
进阶层 最佳实践、性能优化、线程安全
常用 API 一览
| API | 作用 |
|---|---|
makeConstraints | 创建并激活约束 |
updateConstraints | 更新已有约束(保持其他不变) |
remakeConstraints | 移除旧约束,创建新约束 |
removeConstraints | 移除所有约束 |
prepareConstraints | 预创建约束(不激活),用于条件判断 |
常见用法组合
// 1. 边距控制
view.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16))
}
// 2. 相对布局
view1.snp.makeConstraints { make in
make.top.left.equalToSuperview().offset(16)
make.right.equalTo(view2.snp.left).offset(-8)
make.height.equalTo(44)
}
// 3. 倍数与偏移
imageView.snp.makeConstraints { make in
make.width.equalToSuperview().multipliedBy(0.5).offset(-16)
make.height.equalTo(imageView.snp.width).multipliedBy(9.0/16.0)
}
// 4. 优先级设置
label.snp.makeConstraints { make in
make.left.right.equalToSuperview().inset(16)
make.top.equalToSuperview().offset(20)
make.height.greaterThanOrEqualTo(20).priority(.required)
make.height.lessThanOrEqualTo(100).priority(.high)
}
深入层 源码解析、设计思想、扩展定制
核心模块介绍
SnapKit 的架构设计非常精巧,主要包含以下几个核心组件:
- ConstraintMaker:DSL 的入口,提供链式调用接口
- ConstraintItem:封装约束的目标视图和属性
- Constraint:内部表示单个约束的数据结构
- ConstraintAttributes:约束属性的枚举封装
关键协议 ConstraintRelatableTarget 允许约束目标可以是:
- 另一个视图 (
UIView) - 数值 (
CGFloat,Int) - 另一个约束项 (
ConstraintItem)
这种设计使得 SnapKit 的 API 非常灵活,可以写出如 make.width.equalTo(100) 或 make.width.equalTo(otherView) 这样自然的代码。
四、实战演示
下面是一个完整的登录界面布局示例,展示了 SnapKit 在实际业务场景中的应用:
import UIKit
import SnapKit
class LoginViewController: UIViewController {
private let logoImageView = UIImageView()
private let usernameTextField = UITextField()
private let passwordTextField = UITextField()
private let loginButton = UIButton(type: .system)
private let forgotPasswordButton = UIButton(type: .system)
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
setupConstraints()
}
private func setupViews() {
view.backgroundColor = .systemBackground
// Logo
logoImageView.image = UIImage(systemName: "person.circle.fill")
logoImageView.tintColor = .systemBlue
logoImageView.contentMode = .scaleAspectFit
view.addSubview(logoImageView)
// Username
usernameTextField.placeholder = "用户名"
usernameTextField.borderStyle = .roundedRect
usernameTextField.autocapitalizationType = .none
view.addSubview(usernameTextField)
// Password
passwordTextField.placeholder = "密码"
passwordTextField.borderStyle = .roundedRect
passwordTextField.isSecureTextEntry = true
view.addSubview(passwordTextField)
// Login Button
loginButton.setTitle("登录", for: .normal)
loginButton.backgroundColor = .systemBlue
loginButton.setTitleColor(.white, for: .normal)
loginButton.layer.cornerRadius = 8
view.addSubview(loginButton)
// Forgot Password
forgotPasswordButton.setTitle("忘记密码?", for: .normal)
view.addSubview(forgotPasswordButton)
}
private func setupConstraints() {
logoImageView.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide).offset(40)
make.centerX.equalToSuperview()
make.width.height.equalTo(80)
}
usernameTextField.snp.makeConstraints { make in
make.top.equalTo(logoImageView.snp.bottom).offset(40)
make.left.right.equalToSuperview().inset(32)
make.height.equalTo(44)
}
passwordTextField.snp.makeConstraints { make in
make.top.equalTo(usernameTextField.snp.bottom).offset(16)
make.left.right.height.equalTo(usernameTextField)
}
loginButton.snp.makeConstraints { make in
make.top.equalTo(passwordTextField.snp.bottom).offset(24)
make.left.right.equalTo(usernameTextField)
make.height.equalTo(48)
}
forgotPasswordButton.snp.makeConstraints { make in
make.top.equalTo(loginButton.snp.bottom).offset(16)
make.centerX.equalToSuperview()
}
}
}
关键要点:
- 使用
view.safeAreaLayoutGuide适配刘海屏 - 通过
equalTo复用约束,保持代码 DRY - 合理的间距和尺寸,确保界面美观
五、源码亮点
进阶层:值得借鉴的用法
链式调用的实现技巧
SnapKit 通过 @discardableResult 和返回 self 实现链式调用:
// 简化示意
struct ConstraintMaker {
@discardableResult
func equalTo(_ other: ConstraintRelatableTarget) -> ConstraintMaker {
// 设置约束关系
return self
}
@discardableResult
func offset(_ amount: CGFloat) -> ConstraintMaker {
// 设置偏移量
return self
}
}
类型安全的约束目标
使用协议和泛型确保编译期类型检查:
protocol ConstraintRelatableTarget {}
extension UIView: ConstraintRelatableTarget {}
extension CGFloat: ConstraintRelatableTarget {}
extension Int: ConstraintRelatableTarget {}
深入层:设计思想解析
Builder 模式的应用
ConstraintMaker 是 Builder 模式的典型应用:
- 分离构建与表示:DSL 描述意图,内部 Builder 构建实际约束
- 精细控制构建过程:支持
make/update/remake不同构建策略 - 延迟执行:约束在闭包执行完毕后才真正创建和激活
Protocol-Oriented Programming
SnapKit 大量使用协议扩展实现功能:
// 所有视图自动获得 snp 属性
extension UIView {
var snp: ConstraintDSL {
return ConstraintDSL(view: self)
}
}
这种设计让 SnapKit 可以无缝接入任何 UIView 子类,无需继承或修改原有类。
六、踩坑记录
问题 1:约束冲突导致界面异常
症状:控制台输出 "Unable to simultaneously satisfy constraints",界面布局错乱。
原因:SnapKit 自动设置 translatesAutoresizingMaskIntoConstraints = false,但如果视图在 Storyboard 或 Xib 中已有约束,会导致重复约束。
解决:确保代码创建的视图没有在其他地方添加约束,或使用 remakeConstraints 完全重制约束。
// 使用 remakeConstraints 清除旧约束
view.snp.remakeConstraints { make in
make.edges.equalToSuperview()
}
问题 2:updateConstraints 找不到要更新的约束
症状:调用 updateConstraints 时约束没有变化,或控制台报错。
原因:updateConstraints 只能更新已存在的约束。如果约束类型不同(如从 equalTo 改为 lessThanOrEqualTo),需要先用 remakeConstraints。
解决:检查约束类型是否一致,不一致时使用 remakeConstraints。
// ❌ 错误:尝试将 equalTo 更新为 lessThanOrEqualTo
view.snp.makeConstraints { make in
make.width.equalTo(100)
}
view.snp.updateConstraints { make in
make.width.lessThanOrEqualTo(200) // 不会生效
}
// ✅ 正确:使用 remakeConstraints
view.snp.remakeConstraints { make in
make.width.lessThanOrEqualTo(200)
}
问题 3:在 UITableViewCell 中布局问题
症状:Cell 高度计算不正确,或复用时布局错乱。
原因:Cell 的 contentView 是实际容器,约束应该添加到 contentView 而非 Cell 本身。
解决:始终将子视图添加到 contentView,约束也相对于 contentView。
class MyCell: UITableViewCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
let label = UILabel()
contentView.addSubview(label) // 注意是 contentView
label.snp.makeConstraints { make in
make.edges.equalTo(contentView).inset(16) // 相对于 contentView
}
}
}
问题 4:动画更新约束时闪烁
症状:使用 UIView.animate 更新 SnapKit 约束时出现闪烁或跳动。
原因:约束更新和布局刷新时机不正确。
解决:在动画块中先更新约束,然后调用 layoutIfNeeded()。
// ✅ 正确的动画方式
view.snp.updateConstraints { make in
make.width.equalTo(200)
}
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
问题 5:与 SwiftUI 混用时的注意事项
症状:在 UIViewRepresentable 中使用 SnapKit 时约束不生效。
原因:SwiftUI 的生命周期和布局系统与 UIKit 不同。
解决:确保在 makeUIView 中创建约束,在 updateUIView 中使用 updateConstraints 或 remakeConstraints。
struct MyView: UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
let view = UIView()
let subview = UIView()
view.addSubview(subview)
subview.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
// 更新约束
}
}
七、延伸思考
与同类库的横向对比
| 维度 | SnapKit | PureLayout | Cartography |
|---|---|---|---|
| 语言 | Swift | Objective-C/Swift | Swift |
| Stars | 19k+ | 7k+ | 3k+ |
| 维护状态 | ✅ 活跃 | ⚠️ 维护较少 | ⚠️ 已归档 |
| API 风格 | 链式 DSL | 方法调用 | 运算符重载 |
| 学习曲线 | 低 | 中 | 中 |
| SwiftUI 支持 | 需桥接 | 需桥接 | 需桥接 |
推荐使用场景
推荐使用:
- 纯 Swift UIKit 项目
- 需要频繁动态更新布局的场景
- 复杂界面,约束关系较多的页面
- 团队已熟悉 Masonry(OC 版 SnapKit)
不推荐使用:
- 纯 SwiftUI 项目(直接使用 SwiftUI 布局)
- 零依赖要求的 SDK/框架开发
- 极其简单的固定布局(原生代码量差异不大)
关于 Cartography 的说明
Cartography 是另一个流行的 Swift 布局 DSL,使用运算符重载(==、>=、<=)实现约束。虽然 API 非常优雅,但该项目目前已归档不再维护,不建议在新项目中使用。
八、参考资源
- GitHub 仓库: github.com/SnapKit/Sna…
- 官方文档: snapkit.io/docs/
- Masonry(OC 版): github.com/SnapKit/Mas…
- WWDC Session: Auto Layout Techniques in Interface Builder
- 系列 Demo 仓库: github.com/yourname/io…
本期互动
小作业
尝试用 SnapKit 实现一个自适应高度的评论区 Cell,要求:
- 头像在左侧,固定 40x40
- 用户名在头像右侧,单行显示
- 评论内容在用户名下方,多行自适应高度
- 整体边距 16pt
完成后在评论区贴出你的 setupConstraints 方法代码。
思考题
如果你要自己实现一个类似的布局 DSL 库,你会如何设计 API 接口?是像 SnapKit 这样使用闭包和链式调用,还是像 Cartography 那样使用运算符重载?为什么?
读者征集
你在使用 SnapKit 时踩过哪些坑?或者有什么高级用法想分享?欢迎在评论区留言,优质回答会收录进下一期《踩坑记录》。
下一期选题投票:
- A. RxSwift - 响应式编程库
- B. Realm - 移动端数据库
- C. Lottie - 动画渲染库
📅 本系列每周五晚更新 · 已学习:[✓ Alamofire] [✓ Kingfisher] [✓ MarkdownUI] [→ 本期 SnapKit] [○ 第5期]