SnapKit是基于NSLayoutConstraint封装的一个轻量级的布局框架.区别于iOS9.0中苹果引入的系统框架NSLayoutAnchor。其实NSLayoutAnchor是一个工厂类,类似NSNumber这样的设计思想.
开始
当我们开始写约束的时候,一般都从
view.snp.makeConstraints()方法开始,通过点击snp我们进到里面看,发现它是下面这个样子:
// ConstraintView 实际上就是UIView
extension ConstraintView {
public var snp: ConstraintViewDSL {
return ConstraintViewDSL(view: self)
}
}
我们现在知道了实现约束的功能其实跟这个ConstraintViewDSL类有很大关系,这里也是我们理解snapKit库如何实现开始的地方,下面开始详细介绍这个类。
ConstraintViewDSL
查看源码我们发现这个类遵守了一个ConstraintAttributesDSL协议。这个协议里面没有定义属性和方法,默认实现了一些功能:
protocol ConstraintAttributesDSL: ConstraintBasicAttributesDSL { }
extension ConstraintAttributesDSL {
public var top: ConstraintItem {
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.top)
}
public var bottom: ConstraintItem {
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.bottom)
}
...
}
现在我们还不知道ConstraintItem是做什么的,不过没关系,下面会详细说到。接着回到ConstraintViewDSL类里面,可以看到有一些我们常用到的方法:
public func makeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
ConstraintMaker.makeConstraints(item: self.view, closure: closure)
}
public func updateConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
ConstraintMaker.updateConstraints(item: self.view, closure: closure)
}
讲到这里我们可以了解到,项目中常用到的view.snp.makeConstraints() 方法和 view.snp.bottom 其实都是在ConstraintViewDSL类里面定义的。那现在关于ConstraintViewDSL类我们就先讲到这吧,下面我们重点去了解ConstraintMaker和ConstraintItem是干什么的。
ConstraintMaker
写一段最简单的代码,让ConstraintMaker和我们见见面。
view.snp.makeConstraints({make: ConstraintMaker in
make.top.equalTo(20)
})
这是我们有疑问,为什么作者不直接在ConstraintMaker上面写约束呢,给我们ConstraintViewDSL类有什么用呢,下面我们将这种框架核心简单来实现下,站在作者的角度去看待问题:
- 首先我们自己来定义一个ConstraintMaker类,内部实现如下:
class ConstraintMaker {
var item: UIView
var descriptions = [Constraint]()
init(item: UIView) {
self.item = item
}
static func prepareConstraints(item: UIView, closure: (_ make: ConstraintMaker) -> Void) -> [Constraint] {
let maker = ConstraintMaker(item: item)
closure(maker)
return maker.descriptions
}
static func makeConstraints(item: UIView, closure: (_ make: ConstraintMaker) -> Void) {
let constraints = prepareConstraints(item: item, closure: closure)
for constraint in constraints {
constraint.active()
}
}
}
extension ConstraintMaker {
var bottom: ConstraintMakerExtendable {
// 这里先简单实现下ConstraintAttributes
let attr = ConstraintAttributes()
return makeExtendableWithAttributes(attr)
}
var top: ConstraintMakerExtendable {
// 这里先简单实现下ConstraintAttributes
let attr = ConstraintAttributes()
return makeExtendableWithAttributes(attr)
}
private func makeExtendableWithAttributes(_ attributes: ConstraintAttributes) -> ConstraintMakerExtendable {
// 这里存在着将ConstraintAttributes对象转化成了Constraint对象
let constraint = Constraint()
descriptions.append(constraint)
return ConstraintMakerExtendable()
}
}
这些就是snapKit实现约束的核心方法了,为了不报错,这里把剩下的几个辅助的类也定义了,它们内部只实现了一些简单的方法:
class Constraint {
func active() { print("开始布局啦") }
}
class ConstraintMakerExtendable {
func calc() { print("计算约束") }
}
class ConstraintAttributes {
static var top = ConstraintAttributes()
static var bottom = ConstraintAttributes()
}
接着我们还需要定义一个ConstraintViewDSL类,用来承载具体的约束:
class ConstraintViewDSL {
internal let view: UIView
internal init(view: UIView) {
self.view = view
}
public func makeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
ConstraintMaker.makeConstraints(item: self.view, closure: closure)
}
}
extension UIView {
var snp: ConstraintViewDSL {
return ConstraintViewDSL(view: self)
}
}
大功告成,现在我们也可以自己写一个约束库了,在外面调用:
let view1 = UIView()
view1.snp.makeConstraints { (make) in
let bottom: ConstraintMakerExtendable = make.bottom
bottom.calc()
let top: ConstraintMakerExtendable = make.top
top.calc()
}
打印如下:
计算约束
计算约束
开始布局啦
开始布局啦
回到ConstraintMaker中继续讲
SnapKit的作者写了很多的类,当我们第一眼看到这么多类,会觉得无从下手,不知道从哪开始阅读,下面我大概整理了一下,约束的过程。
// 一条完整的约束一般是这样的:
make.top.left.equalTo().offset().priority()
//链式调用的关系如下:
make: ConstraintMaker
.top: ConstraintMakerExtendable
.left: ConstraintMakerExtendable(继承自下面的ConstraintMakerRelatable)
.equalTo: ConstraintMakerRelatable
.offset: ConstraintMakerEditable(继承自下面的ConstraintMakerPriortizable)
.priority: ConstraintMakerPriortizable
// .offset、.priority类继承关系
ConstraintMakerEditable -> ConstraintMakerPriortizable -> ConstraintMakerFinalizable
// .top、.left、.equalTo类继承关系
ConstraintMakerExtendable -> ConstraintMakerRelatable
通过阅读源码可以看到,调用view.snp.makeConstraints()方法,实际上内部是先调用prepareConstraints方法将约束准备好,在调用activate()将约束添加到视图上:
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)
}
}
activeate方法作为添加约束内部实际调用的是:
NSLayoutConstraint.activate(layoutConstraints)
这个方法我们比较熟悉,是系统添加约束的方法,只是activate方法做了一层封装,当然这个方法除了单纯添加约束也可作为更新约束使用,下面会详细讲解到。
上面这种图很形象的表示了约束执行的过程,在makeExtendableWithAttributes方法中,maker对象调用它的.bottom方法(类型为ConstraintMakerExtendable)将约束添加到descriptions数组中,返回ConstraintMakerExtendable类型进行下一次的链式调用,然后获取准备生产的 Array
// maker对象调用makeConstraints方法,开始加工
func makeConstraints() {
// 获得半成品
let constraints = prepareConstraints(item: item, closure: closure)
// 开始加工
for constraint in constraints {
constraint.activateIfNeeded(updatingExisting: false)
}
}
我们继续看ConstraintMaker中的加工的机器:
func makeExtendableWithAttributes(_ attributes: ConstraintAttributes) -> ConstraintMakerExtendable {
// 一条完整的约束描述类 类似 make.top.equal(10)
let description = ConstraintDescription(item: self.item, attributes: attributes)
//将单条约束添加到数组中
self.descriptions.append(description)
// 它可以实现多个属性链式操作 有了它 就可以实现 make.width.height 这种特性
// 其中width是ConstraintMaker的属性 height是ConstraintMakerExtendable的属性
// 它们都是ConstraintMakerExtendable类型
return ConstraintMakerExtendable(description)
}
有了这个ConstraintMakerExtendable类就可以通过链式调用比如.width方法,.height方法添加一些约束,来一步步完善ConstraintDescription类
ConstraintDescription可以看成是Constraint脚手架,在一步步添加约束时操作的都是ConstraintDescription类,等将所有约束添加到数组中,准备下一步生产时,会拿到它内部的constraint属性(Constraint类型)进行操作。
题外话,既然makeConstraints()方法内部执行了两步操作,那我们就可以利用这个特性,在视图有多种布局的时候,可以用到prepareConstraints方法,将布局提前装载好,然后根据状态执行不同显示效果,代码如下:
let v1 = View()
let v2 = View()
self.container.addSubview(v1)
self.container.addSubview(v2)
let constraints = v1.snp.prepareConstraints { (make) -> Void in
make.edges.equalTo(v2)
return
}
//打印 self.container.snp_constraints.count == 0,
for constraint in constraints {
constraint.activate()
}
//打印 self.container.snp_constraints.count == 4,
for constraint in constraints {
constraint.deactivate()
}
//打印 self.container.snp_constraints.count == 0,
再来讲讲 .equalTo()
其实和它类似的方法有很多包括:.equalToSuperview(), .lessThanOrEqualTo(), .lessThanOrEqualToSuperview(), .greaterThanOrEqualTo(), .greaterThanOrEqualToSuperview()实现的功能类似。相同点在是它内部调用的是同一个方法:
func relatedTo(_ other: ConstraintRelatableTarget, relation: ConstraintRelation, file: String, line: UInt) -> ConstraintMakerEditable {
这个方法返回一个ConstraintMakerEditable类型,用于对约束添加附加的操作(offset偏移量,priority优先级)。进入到这个方法内部,我们将核心代码提取出来:
func relatedTo(_ other: ConstraintRelatableTarget) -> ConstraintMakerEditable {
if let other = other as? ConstraintItem {
//这里处理参数类似于equalTo(view.snp.bottom)
} else if let other = other as? ConstraintView {
//这里处理参数类似于equalTo(view)
} else if let other = other as? ConstraintConstantTarget {
//这里处理参数类似于equalTo(50)
} else if let other = other as? ConstraintLayoutGuide {
//这里处理参数类似于equalTo(layoutGuide)
}
}
let v1 = View()
let g1 = UILayoutGuide()
self.container.addSubview(v1)
self.container.addLayoutGuide(g1)
v1.snp.makeConstraints { (make) -> Void in
make.top.equalTo(g1).offset(50)
make.left.equalTo(g1.snp.top).offset(50)
}
如何实现make.top.equalTo(view)和make.top.equalTo(view.snp.top)效果一样?
通过查看ConstraintMakerRelatable类下面的relatedTo()方法,我们可以看到在传入不同类型的参数时(view和view.snp.top分别为UIView类型和ConstraintItem类型),方法内部经过处理,全部转化成了ConstraintItem处理,这时,我们猜想当参数类型是UIView时,是否自动转为了ConstraintItem类型,带着这个疑问我们接着看:
在relatedTo方法中关于视图的判断逻辑是这样子的:
if let other = other as? ConstraintView {
related = ConstraintItem(target: other, attributes: ConstraintAttributes.none)
}
如果传入的是视图对象,则ConstraintItem对象的attributes就置为了.none,应该和这个有关,我们在来了解一下ConstraintAttributes是做什么的?
ConstraintAttributes
这是一个结构体类型,它遵守两个协议,通过这个字面量协议和多选协议,完成以组合的形式加入约束,最后通过layoutAttributes数组,对接到NSLayoutConstraint.Attribute中,下面我们来实现一个:
enum LayoutAttribute: CustomDebugStringConvertible {
case left
case right
case top
case width
var debugDescription: String {
switch self {
case .left: return "左边"
case .right: return "右边"
case .top: return "上边"
case .width: return "宽度"
}
}
}
struct ConstraintAttributes: OptionSet, ExpressibleByIntegerLiteral {
typealias IntegerLiteralType = UInt
var rawValue: UInt
init(rawValue: UInt) {
self.rawValue = rawValue
}
init(_ rawValue: UInt) {
self.init(rawValue: rawValue)
}
init(integerLiteral value: IntegerLiteralType) {
self.init(value)
}
static var none: ConstraintAttributes { return 0 }
static var left: ConstraintAttributes { return 1 }
static var right: ConstraintAttributes { return 2 }
}
extension ConstraintAttributes {
var layoutAttributes:[LayoutAttribute] {
var attrs = [LayoutAttribute]()
if contains(ConstraintAttributes.left) { attrs.append(.left) }
if contains(ConstraintAttributes.right) { attrs.append(.right) }
if contains(ConstraintAttributes.none) { /*什么都不做*/ }
return attrs
}
}
在外面调用:
let attributes: ConstraintAttributes = [.left, .right]
let description = attributes.layoutAttributes.map{ $0.debugDescription }
print(description)
//["左边", "右边"]
另外我们还有一种简便的方式来实现ConstraintAttributes支持多选的方式:
struct ConstraintAttributes: OptionSet {
var rawValue: Int
init(rawValue: Int) {
self.rawValue = rawValue
}
static var none = ConstraintAttributes(rawValue: 1 << 0)
static var left = ConstraintAttributes(rawValue: 1 << 1)
static var right = ConstraintAttributes(rawValue: 1 << 2)
}
我们在实现过程中没有使用字面量协议ExpressibleByIntegerLiteral,直接用的位于运算,和SnapKit作者实现的效果相同。
回到刚才的问题,如果在ConstraintItem构造方法(target: AnyObject?, attributes: ConstraintAttributes)中attributes传入.none,它表示对layoutAttributes数组不添加NSLayoutConstraint.Attribute元素。
因为SnapKit是对NSLayoutConstraint的封装,我们有必要说一说NSLayoutConstraint这个类了,它的构造方法如下:
/*
item: 指定需要添加约束的视图一
attribute: 指定视图一需要约束的属性
relatedBy: 指定视图一和视图二添加约束的关系
toItem: 指定视图一依赖关系的视图二;可为nil
attribute: 指定视图一所依赖的视图二的属性,若view2=nil,该属性设置 NSLayoutAttributeNotAnAttribute
multiplier: 系数
情况一:设置A视图的高度 = A视图高度 * multiplier + constant;此时才会起作用;
情况二:设置A视图和其他视图的关系或 toItem=nil,multiplier设置不等于0即可,若等于0会crash;
constant: 常量
layoutConstraint: 返回生成的约束对象
*/
NSLayoutConstraint(item view1: Any,
attribute attr1: NSLayoutConstraint.Attribute,
relatedBy relation: NSLayoutConstraint.Relation,
toItem view2: Any?,
attribute attr2: NSLayoutConstraint.Attribute,
multiplier: CGFloat,
constant c: CGFloat)
通过文档介绍说该方法实际上就是满足一个数学关系view1.attr1 = view2.attr2 * multiplier + constant
回到刚才的ConstraintItem类,对比NSLayoutConstraint的构造方法,仔细观察它就能发现,我们加的约束无非就是这样的关系:
owningView.ConstraintItem = view1 + attr1
toView.ConstraintItem = view2 + attr2
通过创建两个ConstraintItem就能完成基本的约束。了解这点就知道ConstraintItem的作用了。
通过查看源码发现在Constraint类的便利构造方法中,对NSLayoutConstraint进行了一层封装。包括equalTo(view.snp.top) 和 equalTo(view)实现一样的效果,都在这里做了逻辑处理。
最后再来介绍Constraint类
先来看这句代码
let constraint: Constraint = make.top.equal(20).constraint
我们经常在外面这样使用Constraint这个类,其中ConstraintDescription的作用是用于生产Constraint类,ConstraintDescription的创建在ConstraintMaker类的makeExtendableWithAttributes方法中。
实现一个动画效果
var constraint: Constraint?
view.snp.makeConstraints { (make) in
constraint = make.top.equalToSuperview().offset(10).constraint
}
UIView.animateWithDuration(0.3, {
constraint?.update(inset: 20)
self.container.layoutIfNeeded
})
通常我们查看一个视图下面是否包含约束时,一般会直接调用view.constraints.isEmpty 来判断,严谨来讲某个视图下可能会包含一些约束,但这些约束是"不活跃的", 对视图显示不造成任何影响,所以判断条件需要改一下:
extension UIView {
var isConstraintEmpty: Bool {
return self.constraints.filter { $0.isActive }.isEmpty
}
}