iOS SnapKit源码解析

iOS布局从Frame到Autosizing,再到Autolayout。Storyboard上使用Autolayout对于层级不深的页面来说比较方便,大大提升了开发速度,但App体积也会增加不少,用代码写Autolayout就不是那么方便了。如

1
2
3
4
5
6
7
8
9
10
11
12
// view1已经使用Autolayout布好局了

let view0 = UIView()
view0.backgroundColor = UIColor.gray
view0.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(view0)
let leftConstraint = NSLayoutConstraint(item: view0, attribute: NSLayoutAttribute.left, relatedBy: NSLayoutRelation.equal, toItem: view1, attribute: NSLayoutAttribute.left, multiplier: 1, constant: 100)
let topConstraint = NSLayoutConstraint(item: view0, attribute: NSLayoutAttribute.top, relatedBy: NSLayoutRelation.equal, toItem: self.view, attribute: NSLayoutAttribute.top, multiplier: 1, constant: 20)
let widthConstraint = NSLayoutConstraint(item: view0, attribute: NSLayoutAttribute.width, relatedBy: NSLayoutRelation.equal, toItem: self.view, attribute: NSLayoutAttribute.width, multiplier: 0.5, constant: 0)
let heightConstraint = NSLayoutConstraint(item: view0, attribute: NSLayoutAttribute.height, relatedBy: NSLayoutRelation.equal, toItem: view0, attribute: NSLayoutAttribute.width, multiplier: 1, constant: 0)
let constraints = [leftConstraint, topConstraint, widthConstraint, heightConstraint]
NSLayoutConstraint.activate(constraints)

布好一个控件就要写很长串,还怕写错,比较麻烦。

SnapKit是对Autolayout的封装,是Objective-C Masonry的同一个团体Swift版本。

使用Swift先进语法以及拟多态的方法极大地简化了代码写Autolayout约束,如:

1
2
3
4
5
6
let view1 = UIView()
view1.backgroundColor = UIColor.red
self.view.addSubview(view1)
view1.snp.makeConstraints { (make) in
make.left.top.right.bottom.equalTo(0).offset(0).multipliedBy(1)
}

阅读起来很简单且使用方便

声明ConstraintConstantTarget(ConstraintOffsetTarget、ConstraintInsetTarget),然后通过extension对一些已经存在的或者自定义的类型通过NSLayoutAttribute来转换成NSLayoutConstraint的constant,ConstraintMultiplierTarget通过extension来改变NSLayoutConstraint的multiplier,ConstraintPriorityTarget通过extension来改变NSLayoutConstraint的priority等。

这种编程方法值得学习。

声明ConstraintAttributes,将所有的属性都定义下来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
internal static var none: ConstraintAttributes { return self.init(0) }
internal static var left: ConstraintAttributes { return self.init(1) }
internal static var top: ConstraintAttributes { return self.init(2) }
internal static var right: ConstraintAttributes { return self.init(4) }
internal static var bottom: ConstraintAttributes { return self.init(8) }
internal static var leading: ConstraintAttributes { return self.init(16) }
internal static var trailing: ConstraintAttributes { return self.init(32) }
internal static var width: ConstraintAttributes { return self.init(64) }
internal static var height: ConstraintAttributes { return self.init(128) }
internal static var centerX: ConstraintAttributes { return self.init(256) }
internal static var centerY: ConstraintAttributes { return self.init(512) }
internal static var lastBaseline: ConstraintAttributes { return self.init(1024) }

@available(iOS 8.0, OSX 10.11, *)
internal static var firstBaseline: ConstraintAttributes { return self.init(2048) }

@available(iOS 8.0, *)
internal static var leftMargin: ConstraintAttributes { return self.init(4096) }

@available(iOS 8.0, *)
internal static var rightMargin: ConstraintAttributes { return self.init(8192) }

@available(iOS 8.0, *)
internal static var topMargin: ConstraintAttributes { return self.init(16384) }

@available(iOS 8.0, *)
internal static var bottomMargin: ConstraintAttributes { return self.init(32768) }

@available(iOS 8.0, *)
internal static var leadingMargin: ConstraintAttributes { return self.init(65536) }

@available(iOS 8.0, *)
internal static var trailingMargin: ConstraintAttributes { return self.init(131072) }

@available(iOS 8.0, *)
internal static var centerXWithinMargins: ConstraintAttributes { return self.init(262144) }

@available(iOS 8.0, *)
internal static var centerYWithinMargins: ConstraintAttributes { return self.init(524288) }

// aggregates

internal static var edges: ConstraintAttributes { return self.init(15) }
internal static var size: ConstraintAttributes { return self.init(192) }
internal static var center: ConstraintAttributes { return self.init(768) }

@available(iOS 8.0, *)
internal static var margins: ConstraintAttributes { return self.init(61440) }

@available(iOS 8.0, *)
internal static var centerWithinMargins: ConstraintAttributes { return self.init(786432) }

可以看到每个属性都是2的n次方

通过 var layoutAttributes:[LayoutAttribute] 来获取NSLayoutAttribute集合,这里用到Swift OptionSet类的union、formUnion、subtract和contains语法

ConstraintView的extensions中snp变量返回ConstraintViewDSL对象,ConstraintLayoutGuideDSL是它的子类,提供prepareConstraints、makeConstraints、remakeConstraints、updateConstraints、removeConstraints等方法。

可以看到这些方法都是通过使用工厂类ConstraintMaker来管理约束的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
internal static func makeConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) {
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)
}
for constraint in constraints {
constraint.activateIfNeeded(updatingExisting: false)
}
}

ConstraintMaker的init方法:

1
2
3
4
internal init(item: LayoutConstraintItem) {
self.item = item
self.item.prepare()
}

以left为例(同以上举例 make.left.top.right.bottom.equalTo(0).offset(0).multipliedBy(1) )

1
2
3
4
5
6
7
8
9
public var left: ConstraintMakerExtendable {
return self.makeExtendableWithAttributes(.left)
}

internal func makeExtendableWithAttributes(_ attributes: ConstraintAttributes) -> ConstraintMakerExtendable {
let description = ConstraintDescription(item: self.item, attributes: attributes)
self.descriptions.append(description)
return ConstraintMakerExtendable(description)
}

ConstraintDescription是对NSLayoutConstraint的描述,生成Constraint对象,Constraint对象最终生成约束。

而之所以可以连着调用top、right、bottom得益于ConstraintMakerExtendable类: return self.makeExtendableWithAttributes(.left) 得到ConstraintMakerExtendable类,后面top

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//ConstraintMakerExtendable
public var top: ConstraintMakerExtendable {
self.description.attributes += .top
return self
}

//ConstraintAttributes
internal func +=(left: inout ConstraintAttributes, right: ConstraintAttributes) {
left.formUnion(right)
}

//ConstraintMakerExtendable
public func equalTo(_ other: ConstraintRelatableTarget, _ file: String = #file, _ line: UInt = #line) -> ConstraintMakerEditable {
return self.relatedTo(other, relation: .equal, file: file, line: line)
}

internal func relatedTo(_ other: ConstraintRelatableTarget, relation: ConstraintRelation, file: String, line: UInt) -> ConstraintMakerEditable {
let related: ConstraintItem
let constant: ConstraintConstantTarget

if let other = other as? ConstraintItem {
guard other.attributes == ConstraintAttributes.none ||
other.attributes.layoutAttributes.count <= 1 ||
other.attributes.layoutAttributes == self.description.attributes.layoutAttributes ||
other.attributes == .edges && self.description.attributes == .margins ||
other.attributes == .margins && self.description.attributes == .edges else {
fatalError("Cannot constraint to multiple non identical attributes. (\(file), \(line))");
}

related = other
constant = 0.0
} else if let other = other as? ConstraintView {
related = ConstraintItem(target: other, attributes: ConstraintAttributes.none)
constant = 0.0
} else if let other = other as? ConstraintConstantTarget {
related = ConstraintItem(target: nil, attributes: ConstraintAttributes.none)
constant = other
} else if #available(iOS 9.0, OSX 10.11, *), let other = other as? ConstraintLayoutGuide {
related = ConstraintItem(target: other, attributes: ConstraintAttributes.none)
constant = 0.0
} else {
fatalError("Invalid constraint. (\(file), \(line))")
}

let editable = ConstraintMakerEditable(self.description)
editable.description.sourceLocation = (file, line)
editable.description.relation = relation
editable.description.related = related
editable.description.constant = constant
return editable
}

1
2
3
item1.attribute1 = multiplier × item2.attribute2 + constant

NSLayoutConstraint(item: from, attribute: from属性, relatedBy: related关系, toItem: to, attribute: to属性, multiplier: multiplier, constant: constant)

relatedTo这个方法生成related关系、“to”和“to”的属性和constant

接下来的offset和multipliedBy都是ConstraintMakerEditable类的实例方法

maker.descriptions 的每一个描述都生成Constraint对象,在Constraint的init方法中

1
2
3
4
5
6
7
8
9
let layoutConstraint = LayoutConstraint(
item: layoutFrom,
attribute: layoutFromAttribute,
relatedBy: layoutRelation,
toItem: layoutTo,
attribute: layoutToAttribute,
multiplier: self.multiplier.constraintMultiplierTargetValue,
constant: layoutConstant
)

生成了NSLayoutConstraint对象了,在Constraint类的方法activateIfNeeded中我们也发现了 NSLayoutConstraint.activate(layoutConstraints) 方法了。

-------------本文结束感谢您的阅读-------------