写在前面
经过激烈的思想斗争,笔者从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
用于为已存在的类重新命名,提高代码的可读性。
总而言之,ConstraintView
是为了适配多平台而定义的UIView
或NSView
的别称。
extension ConstraintView
紧接上文,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)
}
}
此处省略了该文件中很多被废弃的方法,只看最关键的变量snp
,此处返回了一个新的对象ConstraintViewDSL
,并以自己,一个ConstraintView
作为参数。
注意:在SnapKit中,几乎所有文件开头都有关于导入UIKit
还是AppKit
的判断,之后就不再展示这段重复的代码。
ConstraintViewDSL
接下来jump到ConstraintViewDSL.swift
文件中,这里只展示它的一个最关键方法:
public struct ConstraintViewDSL: ConstraintAttributesDSL {
public func makeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
ConstraintMaker.makeConstraints(item: self.view, closure: closure)
}
internal let view: ConstraintView
internal init(view: ConstraintView) {
self.view = view
}
}
首先可以看到ConstraintViewDSL
是一个结构体,实现了ConstraintAttributesDSL
接口,构造函数也非常简单,只接收一个ConstraintView
并保存起来;另外,view.snp.makeConstraints
也只是把保存的ConstraintView
,连同传递进来的闭包一起交给ConstraintMaker
处理。
除了makeConstraints
方法,还有remakeConstraints
、updateConstraints
、removeConstraints
等方法,因为都是交给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
,构造一个maker
,然后由闭包调用这个maker
,遍历maker
的descriptions
,将获取的约束添加到一个约束数组constraints
中,然后prepareConstraints
执行完毕并将约束返回这个constraints
,makeConstraints
继续执行,获取这些约束,然后激活。
构造maker
时,传入构造函数的item
应为保存在ConstraintViewDSL
中的ConstraintView
,但在init
声明中变成了LayoutConstraintItem
?
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
可以把 frame ,bouds,center 方式布局的视图自动转化为约束形式,转化的结果就是自动添加需要的约束;而此时我们需要自己添加约束,必然会产生冲突,所以直接指定这个视图不去使用约束布局。
中途休息一下
到目前为止,我们知道了调用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 {
var existingLayoutConstraints: [LayoutConstraint] = []
for constraint in item.constraints {
existingLayoutConstraints += constraint.layoutConstraints
}
for layoutConstraint in layoutConstraints {
let existingLayoutConstraint = existingLayoutConstraints.first { $0 == layoutConstraint }
guard let updateLayoutConstraint = existingLayoutConstraint else {
fatalError("Updated constraint could not find existing matching constraint to update: \(layoutConstraint)")
}
let updateLayoutAttribute = (updateLayoutConstraint.secondAttribute == .notAnAttribute) ? updateLayoutConstraint.firstAttribute : updateLayoutConstraint.secondAttribute
updateLayoutConstraint.constant = self.constant.constraintConstantTargetValueFor(layoutAttribute: updateLayoutAttribute)
}
} else {
NSLayoutConstraint.activate(layoutConstraints)
item.add(constraints: [self])
}
}
这里首先获取了起始目标item
,类型为LayoutConstraintItem
,有变量constraintsSet
来保存所有的约束;
然后获取了自己的layoutConstraints
数组,Constraint
不单指一个约束,而是layoutConstraints
中所有约束的集合,或者说是snp.makeConstraints
过程中的一个集合;
最后通过NSLayoutConstraint.activate
激活了整个layoutConstraints
数组中的约束,并且将这些约束添加到了起始目标的约束集合中保存起来。