iOS SnapKit架构之道(一)makeConstraints的过程
source link: https://rimson.top/2019/09/04/ios-snapkit-1/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
iOS SnapKit架构之道(一)makeConstraints的过程
9月更新:(知乎体)
第一次完成这篇文章是在5月,当时是因为使用了SnapKit而不理解,所以学习了一下简单用法,大致知道了它和AutoLayout的交互。当时的题目还是“源码浅析”,在学习并结束了暑期实习之后,觉得这种标题以及自己的学习方式没有意义,于是提出了疑问:源码浅析到底是在浅析什么?目前我的答案是优雅的框架设计和优雅的Swift用法,所以更新此文,作为“架构之道”的第一篇。如果之后这个答案有了变化,就再做更新吧。
经过激烈的思想斗争,笔者从Android开始并发学习iOS,发现了两者很多的共同之处,这里就不在赘述;不过最大的不适应体现在UI方面,Android的布局编写和预览更舒适。
万般无奈之下,接触到了SnapKit,一个用Swift编写的AutoLayout框架,极大程度上简化了纯布局的代码。
本文只探究makeConstraints
的过程,介绍了基本类和它们的方法转发调用关系,也就是停留在闭包之外,对于链式调用也没有涉及到。
跨平台的ConstraintView
SnapKit的最基本用法:
view.snp.makeConstraints { (make) in
}
首先view.snp
很容易让人想到是使用了扩展,但并不是直接对UIView
的扩展,而是要引入一个新的概念ConstraintView
,具体在ConstraintView.swift
中体现:
#if os(iOS) || os(tvOS)
import UIKit
#else
import AppKit
#endif
#if os(iOS) || os(tvOS)
public typealias ConstraintView = UIView
#else
public typealias ConstraintView = NSView
#endif
这就是该文件所有的代码了,可以看到,通过判断当前系统类型,做了两件事:
- 包的导入:如果当前系统是
iOS
或者tvOS
,那么导入UIKit
,否则导入AppKit
- 类的重命名:如果当前系统是
iOS
或者tvOS
,那么将UIView
重命名为ConstraintView
,否则将NSView
重命名为ConstraintView
。其中typealias
用于为已存在的类重新命名,提高代码的可读性。
view.snp
是对ConstraintView
的扩展,在ConstraintView+Extensions.swift
中返回:
#if os(iOS) || os(tvOS)
import UIKit
#else
import AppKit
#endif
public extension ConstraintView {
// 此处略去很多废弃的方法
public var snp: ConstraintViewDSL {
return ConstraintViewDSL(view: self)
}
}
注意:在SnapKit中,几乎所有文件开头都有关于导入UIKit
还是AppKit
的判断,之后就不再展示这段重复的代码。
此处省略了该文件中很多被废弃的方法,只看最关键的变量snp
,此处返回了一个新的对象ConstraintViewDSL
,并以自己,一个ConstraintView
作为参数。总而言之,ConstraintView
是为了适配多平台而定义的UIView
或NSView
的别称,通过view.snp
对所有平台的View进行操作。
中间人ConstraintViewDSL
接下来jump到ConstraintViewDSL.swift
文件中,DSL意为Domain Specific Language,即领域特定语言,这里展示了常用的三个约束相关的方法:
public struct ConstraintViewDSL: ConstraintAttributesDSL {
public func makeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
ConstraintMaker.makeConstraints(item: self.view, closure: closure)
}
public func remakeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
ConstraintMaker.remakeConstraints(item: self.view, closure: closure)
}
public func updateConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
ConstraintMaker.updateConstraints(item: self.view, closure: closure)
}
internal let view: ConstraintView
internal init(view: ConstraintView) {
self.view = view
}
}
首先可以看到ConstraintViewDSL
是一个结构体,实现了ConstraintAttributesDSL
接口,构造函数也非常简单,只接收一个ConstraintView
作为参数并赋给自己的成员变量保存起来;另外,view.snp.makeConstraints
也只是把保存的ConstraintView
,连同传递进来的闭包一起交给ConstraintMaker
处理。
为什么说ConstraintViewDSL
是中间人呢?跨平台的ConstraintView
通过snp
属性调用了ConstraintViewDSL
,通过这个属性进行约束操作,而真正执行相应操作的是ConstraintMaker
,ConstraintViewDSL
做的工作只是转发。
操作类ConstraintMaker
就像数据库的操作类一样,ConstraintMaker的属性和方法足够帮助我们完成约束的相关操作,ConstraintMaker.swift
文件中:
public class ConstraintMaker {
private let item: LayoutConstraintItem
private var descriptions = [ConstraintDescription]()
internal init(item: LayoutConstraintItem) {
self.item = item
self.item.prepare()
}
internal static func prepareConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) -> [Constraint] {
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)
}
return constraints
}
internal static func makeConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) {
let constraints = prepareConstraints(item: item, closure: closure)
for constraint in constraints {
constraint.activateIfNeeded(updatingExisting: false)
}
}
}
ConstraintMaker
是一个类,从上面展示的代码可以知道创建约束的基本流程:首先makeConstraints
调用prepareConstraints
,在prepareConstraints
中构造一个maker
,将maker
传入闭包执行,再遍历maker
的descriptions
,将获取的约束添加到一个约束数组constraints
中,然后prepareConstraints
执行完毕并将约束返回这个constraints
,makeConstraints
继续执行,获取这些约束,然后逐一激活。
构造maker
时,传入构造函数的item
应为保存在ConstraintViewDSL
中的ConstraintView
,但在init
声明中变成了LayoutConstraintItem
?
从ConstraintView到LayoutConstraintItem
LayoutConstraintItem.swift
:
public protocol LayoutConstraintItem: class {
}
extension ConstraintView : LayoutConstraintItem {
}
可以看到这是一个协议,并且ConstraintView
实现了它,协议中也实现了一些方法,其中就包括prepare
:
extension LayoutConstraintItem {
internal func prepare() {
if let view = self as? ConstraintView {
view.translatesAutoresizingMaskIntoConstraints = false
}
}
}
prepare
方法禁用了从AutoresizingMask
到Constraints
的自动转换,即translatesAutoresizingMaskIntoConstraints
为true
时,可以把 frame ,bouds,center 方式布局的视图自动转化为AutoLayout实现,转化的结果就是自动添加需要的约束。用代码创建的View,该属性默认为true
,而此时我们需要自己添加约束,必然会产生冲突,所以直接指定这个视图不要自动添加约束,所有约束由我们自己添加。
在创建操作类ConstraintMaker
时,构造方法做了两件事:
- 把传入的
ConstraintView
参数转化为LayoutConstraintItem
- 调用
prepare
方法禁用translatesAutoresizingMaskIntoConstraints
。
回到操作类
到目前为止,我们知道了调用view.snp.makeConstraints
时,这个view经过一系列转运,最终禁用了约束布局的自动添加,而这个过程仅仅是prepareConstraints
方法的第一行,也就是只调用了ConstraintMaker
的构造函数,接下来继续分析prepareConstraints
。
internal static func prepareConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) -> [Constraint] {
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)
}
return constraints
}
构造maker
之后,先是执行了闭包的内容(不在本文讨论范围内),紧接着创建了一个包含Constraint
的数组constraints
;然后遍历包含了ConstraintDescription
类型的descriptions
数组(该数组是maker
的成员变量),并试图将每个description
中包含的constraint
添加到constraints
数组中,最后返回该数组。
约束描述ConstraintDescription
ConstraintDescription.swift
:
public class ConstraintDescription {
internal lazy var constraint: Constraint? = {
guard let relation = self.relation,
let related = self.related,
let sourceLocation = self.sourceLocation else {
return nil
}
let from = ConstraintItem(target: self.item, attributes: self.attributes)
return Constraint(
from: from,
to: related,
relation: relation,
sourceLocation: sourceLocation,
label: self.label,
multiplier: self.multiplier,
constant: self.constant,
priority: self.priority
)
}()
}
此处略去了很多成员变量,简单来说,ConstraintDescription
内部持有一个Constraint
变量,需要时可以利这些变量构造出一个Constraint
并返回。
真正的约束Constraint
Constraint.swift
中,关键代码在构造函数,略去成员变量和方法,以及构造函数中关于多平台的适配之后,内容精简如下:
internal init(...) {
...
self.layoutConstraints = []
// get attributes
let layoutFromAttributes = self.from.attributes.layoutAttributes
let layoutToAttributes = self.to.attributes.layoutAttributes
// get layout from
let layoutFrom = self.from.layoutConstraintItem!
// get relation
let layoutRelation = self.relation.layoutRelation
for layoutFromAttribute in layoutFromAttributes {
// get layout to attribute
let layoutToAttribute: NSLayoutAttribute
if layoutToAttributes.count > 0 {
if self.from.attributes == .edges && self.to.attributes == .margins {
switch layoutFromAttribute {
case .left:
layoutToAttribute = .leftMargin
case .right:
layoutToAttribute = .rightMargin
case .top:
layoutToAttribute = .topMargin
case .bottom:
layoutToAttribute = .bottomMargin
default:
fatalError()
}
} else if self.from.attributes == .margins && self.to.attributes == .edges {
switch layoutFromAttribute {
case .leftMargin:
layoutToAttribute = .left
case .rightMargin:
layoutToAttribute = .right
case .topMargin:
layoutToAttribute = .top
case .bottomMargin:
layoutToAttribute = .bottom
default:
fatalError()
}
} else if self.from.attributes == self.to.attributes {
layoutToAttribute = layoutFromAttribute
} else {
layoutToAttribute = layoutToAttributes[0]
}
} else {
if self.to.target == nil && (layoutFromAttribute == .centerX || layoutFromAttribute == .centerY) {
layoutToAttribute = layoutFromAttribute == .centerX ? .left : .top
} else {
layoutToAttribute = layoutFromAttribute
}
}
// get layout constant
let layoutConstant: CGFloat = self.constant.constraintConstantTargetValueFor(layoutAttribute: layoutToAttribute)
// get layout to
var layoutTo: AnyObject? = self.to.target
// use superview if possible
if layoutTo == nil && layoutToAttribute != .width && layoutToAttribute != .height {
layoutTo = layoutFrom.superview
}
// create layout constraint
let layoutConstraint = LayoutConstraint(
item: layoutFrom,
attribute: layoutFromAttribute,
relatedBy: layoutRelation,
toItem: layoutTo,
attribute: layoutToAttribute,
multiplier: self.multiplier.constraintMultiplierTargetValue,
constant: layoutConstant
)
// set label
layoutConstraint.label = self.label
// set priority
layoutConstraint.priority = self.priority.constraintPriorityTargetValue
// set constraint
layoutConstraint.constraint = self
// append
self.layoutConstraints.append(layoutConstraint)
}
}
首先创建layoutConstraints
来保存最后生成的所有LayoutConstraint
(继承自NSLayoutConstraint
),然后获取该约束的起始对象的约束属性layoutFromAttributes
和目标对象的约束属性layoutToAttributes
。接下来的主要逻辑就在循环体内,通过遍历起始对象的约束属性,然后获取目标对象的约束属性,最终创建一条新的约束。
至此,prepareConstraints
执行完毕,makeConstraints
已经获取到了所有需要的约束,接下来要执行最后一步:激活约束。
熟悉的activateIfNeeded
这是Constraint.swift
中的一个方法:
internal func activateIfNeeded(updatingExisting: Bool = false) {
guard let item = self.from.layoutConstraintItem else {
print("WARNING: SnapKit failed to get from item from constraint. Activate will be a no-op.")
return
}
let layoutConstraints = self.layoutConstraints
if updatingExisting {
...
} else {
NSLayoutConstraint.activate(layoutConstraints)
item.add(constraints: [self])
}
}
在其他情况下,remakeConstraints
实际上是先通过removeConstraints
清除之前的约束,然后再通过makeConstraints
添加约束,在这一步是一样的,updatingExisting
也是false
。而updateConstraints
调用activateIfNeeded
时传入了true
,在这个例子中我们先尝试理解makeConstraints
的过程,即updatingExisting
为false
。
这里首先获取了起始目标item
,类型为LayoutConstraintItem
,有成员变量constraintsSet
来保存所有的约束;然后获取了自己的layoutConstraints
数组,此时直接调用了NSLayoutConstraint.activate
激活了整个layoutConstraints
数组中的约束,也就是把所有NSLayoutConstraint
的isActive
属性设置为true
,并且将这些约束添加到了起始目标的约束集合中保存起来。
之所以说熟悉,是因为在 iOS UIView绘制(三)从Layout到Display 中总结了很多关于xxxIfNeeded
的方法,它们的特点都是把View标记为dirty,然后由系统在Main RunLoop中发现并进行相关操作。
创建约束的过程就是先获取闭包中的约束信息(prepareConstraints
),然后逐一激活(activateIfNeeded
)。个人认为SnapKit是一个优雅的框架,使用和设计都比较简洁,简单总结了其中值得以后自己创造类似框架时借鉴的地方:
- 为多平台的不同类型设计通用框架时,应该通过 typealias 等方式进行重新定义,在框架的核心操作部分使用自己定义的类(SnapKit中则为
ConstraintView
),做到不关心下层数据,也避免了繁琐的类型和平台判断。 - 在框架本身和所在的底层平台交互式,尽量符合平台的设计规范和设计习惯,例如SnapKit最后激活
NSLayoutConstraint
时,在activateIfNeeded
的方法命名中有所体现。
Recommend
-
71
README.md
-
77
用了 SnapKit 很久,一开始觉得这就是个很简单的语法糖,后面用着用着还是觉得有点磕磕绊绊,所以又回去看过了一遍官方文档,发现了几个 best practice 是我之前一直没留意到的,就写出来分享一下。
-
61
-
55
我是《架构整洁之道》( Clean Architecture ) 中文版的技术审校者,在审校的过程当中略有感悟,所以希望通过撰写导读的方式分享给大家。 书名的由来 《架构整洁之道》是 Clean Architecture 的...
-
30
-
59
-
35
-
43
ReadMe.md SnapKit SnapKit is a Java UI kit. Check out a demo of SnapKit running in the browser....
-
35
写在前面 2019悄悄溜走一半,无论是离别的忧愁,还是成长路途的艰辛,都在心中滚烫。 距离上一篇文章已经很久了... 懒惰的博主不能将这一切归结于我的时间、我的规划、我的工作,只能怪自己懒......
-
9
上周天,闲来无事,我随意浏览 GitHub 时,偶然发现一个非常流行的库,它有超过 10k 的 commits。我不打算说出其“真名”。即使我了解项目的技术...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK