版本记录
版本号 | 时间 |
---|---|
V1.0 | 2019.07.02 星期二 |
前言
OC中自动布局可以使用三方比如说Masonry,那么Swift呢?Swift也有很优秀的第三方框架,其中SnapKit就很优秀,接下来几篇我们就一起来看下这个框架。
开始
首先看下写作环境
Swift 5, iOS 12, Xcode 10
在本教程中,您将了解SnapKit,一种轻量级DSL(domain-specific language)
,使自动布局和约束变得轻而易举。
Auto Layout
是一种功能强大的工具,用于描述应用程序中不同视图和复杂视图层次结构之间的关系和约束,但是编写这些约束通常在开始时非常不直观。
直到几年前,以编程方式编写这些约束对于使用Visual Formatting Language
或手动创建NSLayoutConstraints
等神秘和冗长的方法非常繁琐。
iOS 9
通过引入布局锚点(Layout Anchors)
大大改进了这些机制,这使得创建约束非常直观。 然而,仍然有很多东西需要让你创建约束更加快捷。 这正是SnapKit
发挥作用的地方!
在本教程中,您将使用SnappyQuiz
- 一个简单的游戏,玩家可以获得随机问题/陈述,并选择是真还是假。
打开SnappyQuiz.xcworkspace
而不是项目文件 - 这很重要。
该项目包含一些您需要的Swift文件:
- QuizViewController.swift:这是屏幕布局发生的地方,包括定义视图。
- QuizViewController + Logic.swift:这个文件包含游戏本身的逻辑。您不需要在本教程中更改此文件。
- QuizViewController + Constraints.swift:屏幕UI的所有约束都位于此文件中,您可以在此处完成大部分工作。
该项目还包括表示游戏状态的State.swift
和找到原始问题数据的Questions.swift
,但在本教程中您不会真正触及这些。
构建并运行项目。您应该看到倒数计时器滴答的第一个问题以及代表当前游戏进度的进度条:
在QuizViewController + Constraints.swift
中,探索setupConstraints()
。此代码使用上述布局锚点来定义应用程序中不同视图之间的关系。
在本教程中,您将使用其SnapKit
变体替换每个约束。
Snappin’ & Chainin’
在您实际修改SnappyQuiz
应用程序之前,您应该更多地了解SnapKit
的实际内容。在本教程的介绍中,我提到SnapKit
使用DSL
,但这究竟意味着什么?
1. What is a DSL?
域特定语言domain-specific language(DSL)
是为表达和处理特定域或解决特定问题而创建的语言。
在SnapKit
的案例中,它旨在创建一种更直观,易于使用的语法,专门用于自动布局(Auto Layout)
约束。
需要了解的一件重要事情是,作为DSL
,SnapKit
主要是syntactic sugar
- 你可以在没有SnapKit
的情况下做任何SnapKit
。但是,SnapKit
提供了更流畅和富有表现力的语法来解决这个特定的领域和问题。
2. SnapKit Basics
采用一组非常常见的约束 - 将视图附加到其所有superview
的边缘:
没有SnapKit
,代码看起来类似于以下内容:
child.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
child.leadingAnchor.constraint(equalTo: parent.leadingAnchor),
child.topAnchor.constraint(equalTo: parent.topAnchor),
child.trailingAnchor.constraint(equalTo: parent.trailingAnchor),
child.bottomAnchor.constraint(equalTo: parent.bottomAnchor),
])
这非常有声明性,但SnapKit
可以做得更好。
SnapKit
在系统中的每个UIView
(和NSView
,在macOS上)引入了一个名为snp
的命名空间。 该命名空间以及makeConstraints(_ :)
方法是SnapKit
的精髓。
SnapKit
表示这样的约束:
child.snp.makeConstraints { make in
make.leading.equalToSuperview()
make.top.equalToSuperview()
make.trailing.equalToSuperview()
make.bottom.equalToSuperview()
}
这可能看起来像是相似数量的代码,但它极大地提高了可读性。 你可能注意到的两件事是:
- 1) 由于
SnapKit
的equalToSuperview()
,您根本不需要引用父项。 这意味着,即使子项移动到不同的父视图,您也不需要修改此代码。 - 2)
make
语法创建了几乎与英语类似的语法,例如“make leading equal to superview“
,这是更好阅读。
3. Composability & Chaining
你刚刚看到了你的第一个SnapKit
代码,但是SnapKit
真正发挥作用的是它的组合功能。 您可以将任何锚点链接在一起,以及约束本身。
您可以将上面的示例重写为:
child.snp.makeConstraints { make in
make.leading.top.trailing.bottom.equalToSuperview()
}
或者更简洁
child.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
想在视图中添加16
的inset
? 另一个简单的链接将帮助你实现:
child.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(16)
}
正如您所看到的,可组合性和链接(composability and chaining)
是SnapKit
的核心,并提供了使用vanilla NSLayoutConstraints无法实现的表现力。
Your First Constraints
现在您已经掌握了SnapKit
的一些基础知识,现在是时候转换setupConstraints()
中的所有约束来使用它了。 它比你想象的要简单得多,你将逐一探讨SnapKit
的各种功能。
返回QuizViewController + Constraints.swift
并找到setupConstraints()
。 您将开始修改updateProgress(to:0)
行以下的约束。 稍后您将回到该行之上的约束。
找到以下代码块,定义计时器label
的约束:
lblTimer.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
lblTimer.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.45),
lblTimer.heightAnchor.constraint(equalToConstant: 45),
lblTimer.topAnchor.constraint(equalTo: viewProgress.bottomAnchor, constant: 32),
lblTimer.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
替换成下面内容
lblTimer.snp.makeConstraints { make in
make.width.equalToSuperview().multipliedBy(0.45) // 1
make.height.equalTo(45) // 2
make.top.equalTo(viewProgress.snp.bottom).offset(32) // 3
make.centerX.equalToSuperview() // 4
}
和以前一样,这是使用SnapKit
的链接语法直接翻译原始约束。 快速进行分解下:
- 1) 使标签的宽度等于父视图的高度,乘以0.45(即超视图宽度的45%)。
- 2) 将标签的高度设置为静态45。
- 3) 将标签的顶部限制在进度条的底部,偏移32。
- 4) 将X轴居中到
superview
的X轴,使标签水平居中。
虽然与基于NSLayoutConstraint
的代码没有太大区别,但它提供了更好的可读性和受约束的视图的范围。
注意:注意其他不同的东西?
SnapKit
不再要求您将translatesAutoresizingMaskIntoConstraints
设置为false
! 库为你做好。 不再忘记这样做并且不知疲倦地调试混乱的约束。
1. Do That Again
转到下一个UI元素 - 问题label
。 找到以下代码:
lblQuestion.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
lblQuestion.topAnchor.constraint(equalTo: lblTimer.bottomAnchor, constant: 24),
lblQuestion.leadingAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16),
lblQuestion.trailingAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16)
])
这里有三个限制。 在这一点上,逐一替换这些可能会让人感到熟悉。 第一个约束可以很容易地转换为:
make.top.equalTo(lblTimer.snp.bottom).offset(24)
最后两个约束也可以用同样的直接方式转化:
make.leading.equalToSuperview().offset(16)
make.trailing.equalToSuperview().offset(-16)
但实际上,您是否注意到这两个约束对leading
锚点和trailing
锚点执行相同的操作? 听起来像一个完美适合一些链接! 使用以下内容替换上面的整个代码块:
lblQuestion.snp.makeConstraints { make in
make.top.equalTo(lblTimer.snp.bottom).offset(24)
make.leading.trailing.equalTo(view.safeAreaLayoutGuide).inset(16)
}
注意两件事:
- 1) 与前面的示例一样,
leading
和trailing
是链接的。 - 2) 您不必总是使用
snp
来约束视图! 注意,这次,你的代码只是为一个好的UILayoutGuide
创建一个约束。
另一个有趣的事实是inset
选项不必是数字。 它也可以采用UIEdgeInsets
结构。 您可以将上面的行重写为:
make.leading.trailing.equalTo(view.safeAreaLayoutGuide)
.inset(UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16))
这在这里可能不太有用,但是当insets
边缘不同时,它会变得非常有用。
两个限制下来,三个去!
2. A Quick Challenge!
下一个约束是您之前已经看到的约束 - 消息label
的边缘应该简单地等于父视图的边缘。 你为什么不亲自尝试这个?
如果您遇到困难,请随时点击下面的按钮查看代码:
替换下面的内容
lblMessage.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ lblMessage.topAnchor.constraint(equalTo: navView.topAnchor), lblMessage.bottomAnchor.constraint(equalTo: navView.bottomAnchor), lblMessage.leadingAnchor.constraint(equalTo: navView.leadingAnchor), lblMessage.trailingAnchor.constraint(equalTo: navView.trailingAnchor) ])
为
lblMessage.snp.makeConstraints { make in make.edges.equalToSuperview() }
3. Final Constraint
移动到SnapKit
的语法仍有一个最终约束。 水平UIStackView
持有True
和False
按钮。
找到以下代码:
svButtons.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
svButtons.leadingAnchor.constraint(equalTo: lblQuestion.leadingAnchor),
svButtons.trailingAnchor.constraint(equalTo: lblQuestion.trailingAnchor),
svButtons.topAnchor.constraint(equalTo: lblQuestion.bottomAnchor, constant: 16),
svButtons.heightAnchor.constraint(equalToConstant: 80)
])
像以前一样,leading and trailing
约束可以链接,因为它们负责相同的关系。 但由于您不想为superview
创建约束,这应该是什么样的?
用以下内容替换上面的代码:
svButtons.snp.makeConstraints { make in
make.leading.trailing.equalTo(lblQuestion)
make.top.equalTo(lblQuestion.snp.bottom).offset(16)
make.height.equalTo(80)
}
注意makeConstraints
闭包中的第一行 - 您只需定义前导和尾随约束应该等于lblQuestion
! 不需要特殊性! SnapKit
能够推断出你指的是lblQuestion
的那些特定约束。
对于更简单的约束也是如此。 以下代码:
view.snp.makeConstraints { make in
make.width.equalTo(otherView.snp.width)
make.centerX.equalTo(otherView.snp.centerX)
}
可以被重写为
view.snp.makeConstraints { make in
make.width.equalTo(otherView)
make.centerX.equalTo(otherView)
}
请注意,不需要otherView
的特性 - SnapKit
根据关系中的第一个视图知道需要创建哪种约束。
您甚至可以通过简单编写来进一步减小代码大小:
view.snp.makeConstraints { make in
make.width.centerX.equalTo(otherView)
}
哇! 多么酷啊?
构建并运行项目。 你会注意到它仍然像以前一样工作。很好~
Modifying Constraints
在本教程的前几节中,您学习了如何创建新约束。但是,有时您想要修改现有约束。
是时候尝试一些您可能想要这样做的用例,以及如何在SnapKit
中实现这一点。
1. Updating a Constraint’s Constant
一些SnappyQuiz
的用户对切换到横向时应用程序的外观非常沮丧。
当应用程序切换方向时,您可以通过修改UI的某些方面来使其更好,这样您就可以做到这一点。
对于此任务,您将以横向方向增加倒数计时器的高度,并增加字体大小。在此特定上下文中,您需要更新计时器标签的高度约束的常量。
当你只对更新常量感兴趣时,SnapKit
有一个名为updateConstraints(_ :)
的超级有用的方法,它在这里非常适合。
回到QuizViewController + Constraints.swift
,在文件末尾添加以下代码:
// MARK: - Orientation Transition Handling
extension QuizViewController {
override func willTransition(
to newCollection: UITraitCollection,
with coordinator: UIViewControllerTransitionCoordinator
) {
super.willTransition(to: newCollection, with: coordinator)
// 1
let isPortrait = UIDevice.current.orientation.isPortrait
// 2
lblTimer.snp.updateConstraints { make in
make.height.equalTo(isPortrait ? 45 : 65)
}
// 3
lblTimer.font = UIFont.systemFont(ofSize: isPortrait ? 20 : 32, weight: .light)
}
}
这增加了一个扩展,它将处理视图控制器的旋转。 这是代码的作用:
- 1) 确定设备的当前方向
- 2) 使用
updateConstraints(_ :)
并将计时器标签的高度更新为45(如果它是纵向) - 否则,将其设置为65。 - 3) 最后,根据方向相应地增加字体大小。
以为会很难吗? 抱歉让你失望!
构建并运行项目。 应用程序在模拟器中启动后,按Command-Right Arrow
或Command-Left Arrow
更改设备方向。 请注意label
如何根据设备的方向增加其高度和字体大小。
2. Remaking Constraints
有时,您需要的不仅仅是修改一些常量。 您可能希望完全更改特定视图上的整个约束集。 对于那种非常常见的情况,SnapKit
有另一个有用的方法 - 你猜对了--rekekeConstraints(_ :)
。
在SnappyQuiz
中有一个完美的地方可以尝试这种方法:顶部的进度条。 现在,进度条的宽度约束保存在QuizViewController.swift
中名为progressConstraints
的变量中。 然后,updateProgress(to :)
简单地销毁旧约束并创建一个新约束。
是时候看看你是否可以让这个烂摊子好一点。
回到QuizViewController + Constraints.swift
,看看updateProgress(to:)
。 它检查是否已存在约束,如果是,则将其停用。 然后,它创建一个新约束并激活它。
用以下内容替换updateProgress(to :)
:
func updateProgress(to progress: Double) {
viewProgress.snp.remakeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide)
make.width.equalToSuperview().multipliedBy(progress)
make.height.equalTo(32)
make.leading.equalToSuperview()
}
}
哇,这更好! 整个有点神秘的代码片段完全被几行代码所取代。 remakeConstraints(_ :)
只是每次都替换整个约束集,因此您不必手动引用约束并对其进行管理。
另一个好处是你可以进一步清理当前代码中的一些混乱。
在setupConstraints()
中,删除以下代码:
guard let navView = navigationController?.view else { return }
viewProgress.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
viewProgress.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
viewProgress.heightAnchor.constraint(equalToConstant: 32),
viewProgress.leadingAnchor.constraint(equalTo: view.leadingAnchor)
])
该方法的第一行现在应该只是updateProgress(to:0)
。
最后,您可以在QuizViewController.swift
中删除以下行:
/// Progress bar constraint
var progressConstraint: NSLayoutConstraint!
全部完成! 构建并运行您的应用程序,一切都应该像以前一样工作,但具有更清晰的约束管理代码。
3. Keeping a Reference
虽然你不会在SnappyQuiz
中试验这个选项,但它仍然是你应该知道的。
在标准的NSLayoutConstraint
方式中,您可以存储对约束的引用并在以后修改它。 SnapKit
也可以使用Constraint
类型:
var topConstraint: Constraint?
lblTimer.snp.makeConstraints { make in
// Store your constraint
self.topConstraint = make.top.equalToSuperview().inset(16)
make.leading.trailing.bottom.equalToSuperView()
}
// Which you can later modify
self.topConstraint?.update(inset: 32)
// Or entirely deactivate
self.topConstraint?.deactivate()
When Things Go Wrong
有时在生活中,事情出了问题。 在谈论自动布局(Auto Layout)
约束时,情况更是如此。
回到QuizViewController+Constraints.swift
,找到以下行:
make.centerX.equalToSuperview()
在它下面,但仍然在makeConstraints
闭包内,添加:
make.centerY.equalToSuperview()
构建并运行应用程序。 正如您所看到的,UI完全被破坏了:
此外,正如预期的那样,您将在调试控制台中看到一个破坏约束的巨大墙,其外观应类似于以下内容:
[LayoutConstraints] Unable to simultaneously satisfy constraints.
"<SnapKit.LayoutConstraint:0x600001b251a0@QuizViewController+Constraints.swift#62 UIView:0x7f9371e004a0.top == UILayoutGuide:0x60000062c0e0.top>",
"<SnapKit.LayoutConstraint:0x600001b25260@QuizViewController+Constraints.swift#64 UIView:0x7f9371e004a0.height == 32.0>",
"<SnapKit.LayoutConstraint:0x600001b2dc80@QuizViewController+Constraints.swift#38 UILabel:0x7f9371e088c0.height == 45.0>",
"<SnapKit.LayoutConstraint:0x600001b2dce0@QuizViewController+Constraints.swift#39 UILabel:0x7f9371e088c0.top == UIView:0x7f9371e004a0.bottom + 32.0>",
"<SnapKit.LayoutConstraint:0x600001b2dda0@QuizViewController+Constraints.swift#41 UILabel:0x7f9371e088c0.centerY == UIView:0x7f9371e09a50.centerY>",
"<NSLayoutConstraint:0x600001c6c2d0 'UIView-Encapsulated-Layout-Height' UIView:0x7f9371e09a50.height == 551 (active)>",
"<NSLayoutConstraint:0x600001c61450 'UIViewSafeAreaLayoutGuide-top' V:|-(0)-[UILayoutGuide:0x60000062c0e0'UIViewSafeAreaLayoutGuide'] (active, names: '|':UIView:0x7f9371e09a50 )>"
Will attempt to recover by breaking constraint
<SnapKit.LayoutConstraint:0x600001b2dc80@QuizViewController+Constraints.swift#38 UILabel:0x7fc53e41d060.height == 45.0>
好家伙。 你甚至从哪里开始? 所有你看到的是一堆内存地址,并不一定意味着太多。 理解哪些约束被打破也很困难。
幸运的是,SnapKit
提供了一个很好的附加修饰符来跟踪这些问题,称为labeled(_:)
。
用以下内容替换整个lblTimer
约束块:
lblTimer.snp.makeConstraints { make in
make.width.equalToSuperview().multipliedBy(0.45).labeled("timerWidth")
make.height.equalTo(45).labeled("timerHeight")
make.top.equalTo(viewProgress.snp.bottom).offset(32).labeled("timerTop")
make.centerX.equalToSuperview().labeled("timerCenterX")
make.centerY.equalToSuperview().labeled("timerCenterY")
}
注意到每个约束的labeled(_:)
添加? 这允许您为每个约束附加描述性标题,因此您不必选择内存地址并失去理智。
最后一次构建并运行您的应用程序。 你破坏的约束在这一点上应该提供更清晰的信息:
[LayoutConstraints] Unable to simultaneously satisfy constraints.
"<SnapKit.LayoutConstraint:0x60000365c4e0@QuizViewController+Constraints.swift#62 UIView:0x7fc53e4181d0.top == UILayoutGuide:0x600002b0ae60.top>",
"<SnapKit.LayoutConstraint:0x60000365e8e0@QuizViewController+Constraints.swift#64 UIView:0x7fc53e4181d0.height == 32.0>",
"<SnapKit.LayoutConstraint:timerCenterY@QuizViewController+Constraints.swift#41 UILabel:0x7fc53e41d060.centerY == UIView:0x7fc4fe507170.centerY>",
"<SnapKit.LayoutConstraint:timerHeight@QuizViewController+Constraints.swift#38 UILabel:0x7fc53e41d060.height == 45.0>",
"<SnapKit.LayoutConstraint:timerTop@QuizViewController+Constraints.swift#39 UILabel:0x7fc53e41d060.top == UIView:0x7fc53e4181d0.bottom + 32.0>",
"<NSLayoutConstraint:0x6000031346e0 'UIView-Encapsulated-Layout-Height' UIView:0x7fc4fe507170.height == 551 (active)>",
"<NSLayoutConstraint:0x600003139c70 'UIViewSafeAreaLayoutGuide-top' V:|-(0)-[UILayoutGuide:0x600002b0ae60'UIViewSafeAreaLayoutGuide'] (active, names: '|':UIView:0x7fc4fe507170 )>"
Will attempt to recover by breaking constraint
<SnapKit.LayoutConstraint:timerHeight@QuizViewController+Constraints.swift#38 UILabel:0x7fc53e41d060.height == 45.0>
这看起来很相似,但要仔细看。 你可以看到像timerCenterY
这样的宝石。 这提供了更多信息,并且您有一些很好的标记限制来开始调试。
更具体地说,您可以在此输出中识别的唯一三个标签是timerCenterY
,timerHeight
和timerTop
。 由于高度是静态的,您可以确定冲突是在两个约束之间留下的。 这比缩小自动布局调试输出的原始混乱要快得多!
完成后,随意删除开始此混乱的centerY
约束。
恭喜! 您现在已经了解了SnapKit
所提供的大部分内容,但您仍需要考虑一些功能和修饰符,例如priority, divided
等等。 查看SnapKit的官方GitHub仓库SnapKit’s official GitHub repo了解更多信息。
请记住,SnapKit
可以帮助您创建一个易于使用,特定于问题的语法来创建约束,但它不提供常规NSLayoutConstraints
无法实现的功能。 随意尝试两者并找到适合每种情况的良好中间立场。
后记
本篇主要讲述了
SnapKit
自动布局的框架,感兴趣的给个赞或者关注~~~
网友评论