iOS SnapKit架构之道(一)makeConstraints的过程
status
category
date
summary
slug
icon
tags
password
9月更新:(知乎体)
第一次完成这篇文章是在5月,当时是因为使用了SnapKit而不理解,所以学习了一下简单用法,大致知道了它和AutoLayout的交互。当时的题目还是“源码浅析”,在学习并结束了暑期实习之后,觉得这种标题以及自己的学习方式没有意义,于是提出了疑问:源码浅析到底是在浅析什么?目前我的答案是优雅的框架设计和优雅的Swift用法,所以更新此文,作为“架构之道”的第一篇。如果之后这个答案有了变化,就再做更新吧。
经过激烈的思想斗争,笔者从Android开始并发学习iOS,发现了两者很多的共同之处,这里就不在赘述;不过最大的不适应体现在UI方面,Android的布局编写和预览更舒适。
万般无奈之下,接触到了SnapKit,一个用Swift编写的AutoLayout框架,极大程度上简化了纯布局的代码。
本文只探究
makeConstraints
的过程,介绍了基本类和它们的方法转发调用关系,也就是停留在闭包之外,对于链式调用也没有涉及到。跨平台的ConstraintView
SnapKit的最基本用法:
首先
view.snp
很容易让人想到是使用了扩展,但并不是直接对UIView
的扩展,而是要引入一个新的概念ConstraintView
,具体在ConstraintView.swift
中体现:这就是该文件所有的代码了,可以看到,通过判断当前系统类型,做了两件事: 1. 包的导入:如果当前系统是
iOS
或者tvOS
,那么导入UIKit
,否则导入AppKit
2. 类的重命名:如果当前系统是iOS
或者tvOS
,那么将UIView
重命名为ConstraintView
,否则将NSView
重命名为ConstraintView
。其中typealias
用于为已存在的类重新命名,提高代码的可读性。view.snp
是对ConstraintView
的扩展,在ConstraintView+Extensions.swift
中返回:注意:在SnapKit中,几乎所有文件开头都有关于导入
UIKit
还是AppKit
的判断,之后就不再展示这段重复的代码。此处省略了该文件中很多被废弃的方法,只看最关键的变量
snp
,此处返回了一个新的对象ConstraintViewDSL
,并以自己,一个ConstraintView
作为参数。总而言之,ConstraintView
是为了适配多平台而定义的UIView
或NSView
的别称,通过view.snp
对所有平台的View进行操作。中间人ConstraintViewDSL
接下来jump到
ConstraintViewDSL.swift
文件中,DSL意为Domain Specific Language,即领域特定语言,这里展示了常用的三个约束相关的方法:首先可以看到
ConstraintViewDSL
是一个结构体,实现了ConstraintAttributesDSL
接口,构造函数也非常简单,只接收一个ConstraintView
作为参数并赋给自己的成员变量保存起来;另外,view.snp.makeConstraints
也只是把保存的ConstraintView
,连同传递进来的闭包一起交给ConstraintMaker
处理。为什么说
ConstraintViewDSL
是中间人呢?跨平台的ConstraintView
通过snp
属性调用了ConstraintViewDSL
,通过这个属性进行约束操作,而真正执行相应操作的是ConstraintMaker
,ConstraintViewDSL
做的工作只是转发。操作类ConstraintMaker
就像数据库的操作类一样,ConstraintMaker的属性和方法足够帮助我们完成约束的相关操作,
ConstraintMaker.swift
文件中:ConstraintMaker
是一个类,从上面展示的代码可以知道创建约束的基本流程:首先makeConstraints
调用prepareConstraints
,在prepareConstraints
中构造一个maker
,将maker
传入闭包执行,再遍历maker
的descriptions
,将获取的约束添加到一个约束数组constraints
中,然后prepareConstraints
执行完毕并将约束返回这个constraints
,makeConstraints
继续执行,获取这些约束,然后逐一激活。构造
maker
时,传入构造函数的item
应为保存在ConstraintViewDSL
中的ConstraintView
,但在init
声明中变成了LayoutConstraintItem
?从ConstraintView到LayoutConstraintItem
LayoutConstraintItem.swift
:可以看到这是一个协议,并且
ConstraintView
实现了它,协议中也实现了一些方法,其中就包括prepare
:prepare
方法禁用了从AutoresizingMask
到Constraints
的自动转换,即translatesAutoresizingMaskIntoConstraints
为true
时,可以把 frame ,bouds,center 方式布局的视图自动转化为AutoLayout实现,转化的结果就是自动添加需要的约束。用代码创建的View,该属性默认为true
,而此时我们需要自己添加约束,必然会产生冲突,所以直接指定这个视图不要自动添加约束,所有约束由我们自己添加。在创建操作类
ConstraintMaker
时,构造方法做了两件事:- 把传入的
ConstraintView
参数转化为LayoutConstraintItem
- 调用
prepare
方法禁用translatesAutoresizingMaskIntoConstraints
。
回到操作类
到目前为止,我们知道了调用
view.snp.makeConstraints
时,这个view经过一系列转运,最终禁用了约束布局的自动添加,而这个过程仅仅是prepareConstraints
方法的第一行,也就是只调用了ConstraintMaker
的构造函数,接下来继续分析prepareConstraints
。构造
maker
之后,先是执行了闭包的内容(不在本文讨论范围内),紧接着创建了一个包含Constraint
的数组constraints
;然后遍历包含了ConstraintDescription
类型的descriptions
数组(该数组是maker
的成员变量),并试图将每个description
中包含的constraint
添加到constraints
数组中,最后返回该数组。约束描述ConstraintDescription
ConstraintDescription.swift
:此处略去了很多成员变量,简单来说,
ConstraintDescription
内部持有一个Constraint
变量,需要时可以利这些变量构造出一个Constraint
并返回。真正的约束Constraint
Constraint.swift
中,关键代码在构造函数,略去成员变量和方法,以及构造函数中关于多平台的适配之后,内容精简如下:首先创建
layoutConstraints
来保存最后生成的所有LayoutConstraint
(继承自NSLayoutConstraint
),然后获取该约束的起始对象的约束属性layoutFromAttributes
和目标对象的约束属性layoutToAttributes
。接下来的主要逻辑就在循环体内,通过遍历起始对象的约束属性,然后获取目标对象的约束属性,最终创建一条新的约束。至此,
prepareConstraints
执行完毕,makeConstraints
已经获取到了所有需要的约束,接下来要执行最后一步:激活约束。熟悉的activateIfNeeded
这是
Constraint.swift
中的一个方法:在其他情况下,
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
的方法命名中有所体现。
Loading...