0

iOS SnapKit架构之道(一)makeConstraints的过程

 2 years ago
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的过程

发表于

2019-09-04 分类于 技术

Valine: 0

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

这就是该文件所有的代码了,可以看到,通过判断当前系统类型,做了两件事:

  1. 包的导入:如果当前系统是iOS或者tvOS,那么导入UIKit,否则导入AppKit
  2. 类的重命名:如果当前系统是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是为了适配多平台而定义的UIViewNSView的别称,通过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,通过这个属性进行约束操作,而真正执行相应操作的是ConstraintMakerConstraintViewDSL做的工作只是转发。

操作类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传入闭包执行,再遍历makerdescriptions,将获取的约束添加到一个约束数组constraints中,然后prepareConstraints执行完毕并将约束返回这个constraintsmakeConstraints继续执行,获取这些约束,然后逐一激活。

构造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方法禁用了从AutoresizingMaskConstraints的自动转换,即translatesAutoresizingMaskIntoConstraints true时,可以把 frame ,bouds,center 方式布局的视图自动转化为AutoLayout实现,转化的结果就是自动添加需要的约束。用代码创建的View,该属性默认为true,而此时我们需要自己添加约束,必然会产生冲突,所以直接指定这个视图不要自动添加约束,所有约束由我们自己添加。

在创建操作类ConstraintMaker时,构造方法做了两件事:

  1. 把传入的ConstraintView参数转化为LayoutConstraintItem
  2. 调用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的过程,即updatingExistingfalse

这里首先获取了起始目标item,类型为LayoutConstraintItem,有成员变量constraintsSet来保存所有的约束;然后获取了自己的layoutConstraints数组,此时直接调用了NSLayoutConstraint.activate激活了整个layoutConstraints数组中的约束,也就是把所有NSLayoutConstraintisActive属性设置为true,并且将这些约束添加到了起始目标的约束集合中保存起来。

之所以说熟悉,是因为在 iOS UIView绘制(三)从Layout到Display 中总结了很多关于xxxIfNeeded的方法,它们的特点都是把View标记为dirty,然后由系统在Main RunLoop中发现并进行相关操作。

创建约束的过程就是先获取闭包中的约束信息(prepareConstraints),然后逐一激活(activateIfNeeded)。个人认为SnapKit是一个优雅的框架,使用和设计都比较简洁,简单总结了其中值得以后自己创造类似框架时借鉴的地方:

  • 为多平台的不同类型设计通用框架时,应该通过 typealias 等方式进行重新定义,在框架的核心操作部分使用自己定义的类(SnapKit中则为ConstraintView),做到不关心下层数据,也避免了繁琐的类型和平台判断。
  • 在框架本身和所在的底层平台交互式,尽量符合平台的设计规范和设计习惯,例如SnapKit最后激活NSLayoutConstraint时,在activateIfNeeded的方法命名中有所体现。

Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK