Snapkit是一个AutoLayout的封装库,是Masonary在Swift中的代替品。通过SnapKit,我们可以方便的进行UI的操作。
众所周知,苹果提供了AutoLayout方便了UI设计,但是官方提供的Api极度蛋疼, 为了一个四边相等,我们需要书写如下数据:
marqueeLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: 0).isActive = true
marqueeLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: 0).isActive = true
marqueeLabel.topAnchor.constraint(equalTo: topAnchor, constant: 0).isActive = true
marqueeLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0).isActive = true
使用Snapkit之后,就可以改成如下数据
marqueeLabel.snp.makeConstraints {(make) in
make.edges.equalToSuperview()
}
1、snp
ConstraintView+Extensions.swift
public extension ConstraintView {
public var snp: ConstraintViewDSL {
return ConstraintViewDSL(view: self)
}
}
可以看到snp是ConstraintView扩展中提供的功能, ConstraintView本身是UIView的别名
涉及类:ConstraintView、ConstrainDSL
1.1、ConstraintView
ConstraintView是外观模式的一个标准应用,通过别名,它混一了iOS的UIView与MacOS的NSView。
#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
继承协议:ConstraintRelatableTarget
1.1.1、ConstraintRelatableTarget
Snapkit库中,对外观模式的应用是十分值得学习的地方。
在设置约束的过程中,会使用多种类型的数据:
make.edges.equalTo(view1.snp.bottom)
make.edges.equalTo(view1)
make.size.equalTo(10)
make.size.equalTo(CGPoint(x: 10, y: 10))
可以看到,equalTo()函数的参数存在许许多多的数据类型,Snapkit通过协议ConstraintRelatableTarget对其进行了方便的封装。
public protocol ConstraintRelatableTarget {
}
extension Int: ConstraintRelatableTarget {
}
......
extension ConstraintInsets: ConstraintRelatableTarget {
}
extension ConstraintItem: ConstraintRelatableTarget {
}
extension ConstraintView: ConstraintRelatableTarget {
}
ps:
UIView在Snapkit中的别名:
结构体:ConstraintView,
协议: LayoutConstraintItem,添加各种约束
包装者:ConstraintItem,将View与Attribute混为一体
2、makeConstraints
marqueeLabel.snp.~~**makeConstraints**~~
updateConstraints,removeConstraints,makeConstraints 这一系列方法就不展开了。
顾名思义,makeConstraints函数中生成了NSLayout的约束。
public struct ConstraintViewDSL {
public func makeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
ConstraintMaker.makeConstraints(item: self.view, closure: closure)
}
}
可以看出,ConstraintViewDSL中的makeConstraints是对ConstraintMaker的makeConstraints进行的封装。
涉及类 ConstraintViewDSL,ConstraintMaker
2.1、ConstraintViewDSL
本类的初始化需要ConstraintView,也就是marqueeLabel
封装了prepareConstraints,makeConstraints,remakeConstraints,updateConstraints,removeConstraints等 一系列处理方法。它们统一都是调用ConstraintMaker类的静态同名方法,只是将ConstraintView作为第一个参数统一进去。
继承类:ConstraintAttributesDSL,ConstraintBasicAttributesDSL
2.1.1、ConstraintAttributesDSL,ConstraintBasicAttributesDSL
涉及类 ConstraintItem
这两个类可以放在一起说,它们都是提供了生成ConstraintItem的数据。
public var left: ConstraintItem {
return ConstraintItem(
target: self.target,
attributes: ConstraintAttributes.left
)
}
可以猜出,这里是和
make.left.equalToSuperview()
相关的,至于如何使用,我们会在生成约束的地方来讲。
2.1.2、ConstraintItem
ConstraintItem很好理解,就是将目标(target)和约束 (attributes)绑定在一起。
可以看到target是AnyObject,这是因为target不一定是UIView.下面我们讲到Constraint类时,会发现Constraint的from,to属性都是ConstraintItem,target在from中一般是一个UIView,而在to中,由于有make.width.equalTo(100)这种属性存在,ConstraintItem可能没有target。
3、ConstraintMaker
ConstraintMaker 是Snapkit的核心类,在本类中生成了约束
让我们来看一下makeConstraints的定义:(展开版)
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)
}
}
本类主要分成了生成约束描述与约束描述翻译两个部分:
3.1、生成约束描述
第一部分:约束定义,通过makeExtendableWithAttributes生成一系列ConstraintMakerExtendable 型数据:
关键类 ConstraintDescription
**涉及类 ConstraintAttributes, ConstraintDescription,ConstraintMakerExtendable,ConstraintMakerRelatable **
1. private var descriptions = [ConstraintDescription]()
2. public var left: ConstraintMakerExtendable {
return self.makeExtendableWithAttributes(.left)
}
3. func makeExtendableWithAttributes(_ attributes: ConstraintAttributes) -> ConstraintMakerExtendable {
let description = ConstraintDescription(item: self.item, attributes: attributes)
self.descriptions.append(description)
return ConstraintMakerExtendable(description)
}
由此可知
marqueeLabel.snp.makeConstraints {(make) in
~~** make.left.equalToSuperview()**~~
}
{(make:ConstraintMaker) in
let maker = ConstraintMaker(item: item)
let description = ConstraintDescription(
item: marqueeLabel,
attributes: ConstraintAttributes.left
)
self.descriptions.append(description)
let extend : ConstraintMakerExtendable =
ConstraintMakerExtendable(description)
extend.equalToSuperview()
}
3.1.1 ConstraintAttributes
在此类中,封装了 Snapkit 到 LayoutAttribute 的转换
**继承协议 OptionSet, ExpressibleByIntegerLiteral **
通过这两个协议,使得ConstraintAttributes可以通过 Int,Int数组,枚举,枚举数组等各种形式初始化.可以通过container决定是否存在数据。
在ConstraintAttributes中,提供了一系列left,right之类的静态成员。
static var left: ConstraintAttributes {
return 1
}
LayoutAttribute类似ConstraintView,也是通过外观模式混一了iOS,MacOS,多种版本,可以简单的对标成NSLayoutConstraint。
var layoutAttributes:[LayoutAttribute] 就是协议NSLayoutConstraint的数组。
var layoutAttributes:[LayoutAttribute] {
var attrs = [LayoutAttribute]()
if (self.contains(ConstraintAttributes.left)) {
attrs.append(.left)
}
if (self.contains(ConstraintAttributes.top)) {
attrs.append(.top)
}
}
可以看到,根据自身是否含有静态成员,NSLayoutAttribute存储到了ConstraintAttributes的layoutAttributes属性中。
也就是说
ConstraintAttributes.left 中存在一个
marqueeLabel.leftAnchor
3.1.2 ConstraintDescription
ConstraintDescription 是约束的描述者,
约束的生成过程,就是从
make.top.lessThanOrEqualTo(imageView.snp.bottom).offset(20).multipliedBy(2).priorityLow()
这样的语句转成ConstraintDescription的过程
let description = ConstraintDescription(
item: marqueeLabel,
attributes: ConstraintAttributes.left
)
self.descriptions.append(description)
可以看出ConstraintDescription通过item与ConstraintAttributes初始化,
来看下的类图
对其中的各个属性,我们来一一解释:
item: LayoutConstraintItem
attribute: ConstraintAttributes
这两个属性在初始化时写入
relation: ConstraintRelation
一个简单的枚举,对应到 NSLayoutConstraint.Relation 的三种状态
case .equal:
return .equal
case .lessThanOrEqual:
return .lessThanOrEqual
case .greaterThanOrEqual:
return .greaterThanOrEqual
对应了 lessThanOrEqualTo
related: ConstraintItem
ConstraintItem包裹LayoutConstraintItem与ConstraintAttributes,用来判断是否数据是否一致
对应了 imageView.snp.bottom
multiplier: ConstraintMultiplierTarget
ConstraintMultiplierTarget将Int,UInt,Float,CGFloat等一系列数据转换为CGFloat
对应.multipliedBy(2)
constant: ConstraintConstantTarget
这个需要结合ConstraintInsetTarget : ConstraintConstantTarget来看
ConstraintConstantTarget将Int,UInt,Float,CGFloat, UIEdgeInsets, CGPoint, CGSize 等一系列数据转换为需要的约束值CGFloat:
extension ConstraintConstantTarget {
internal func constraintConstantTargetValueFor(layoutAttribute: LayoutAttribute) -> CGFloat
对应.offset(20)
priority: ConstraintPriorityTarget
ConstraintPriorityTarget,UInt,Float,CGFloat等一系列数据转换为UILayoutPriority
对应.priorityLow()
constraint: Constraint?
这是一个懒加载的数据,对约束的操作都放在这个里面进行,我们到最后再来讲解
当前数据只写入了前两个成员变量,之后成员变量如何写入会慢慢介绍。
3.1.3 ConstraintMakerExtendable
**继承类 ConstraintMakerRelatable **
本方法提供的是链式调用
make.left.right.top.....
make.left 我们已经知道,等价于生成一个ConstraintMakerExtendable
let ext: ConstraintMakerExtendable = self.makeExtendableWithAttributes(.left)
而后续的.right.top.....则是
public class ConstraintMakerExtendable: ConstraintMakerRelatable {
public var left: ConstraintMakerExtendable {
self.description.attributes += .left
return self
}
}
let ext: ConstraintMakerExtendable = self.makeExtendableWithAttributes(.left)
ext.description.attributes += .right
ext.description.attributes += .top
3.1.4 ConstraintMakerRelatable
提供 equalTo, equalToSuperview, ....等一系列方法
**继承类 ConstraintMakerEditable **
这一系列方法都是基于 relateTo 函数
之前我们说过在
let description = ConstraintDescription(
item: marqueeLabel,
attributes: ConstraintAttributes.left
)
self.descriptions.append(description)
此时description还只填写了前两个成员变量。
在relatedTo 函数根据other与relation的类型 ,填写description中的数据:
closure | 类型 | 生成数据 |
---|---|---|
top.equalTo(a.snp.bottom) | ConstraintItem | related = other,constant = 0.0 |
top.equalToSuperview() | ConstraintView | related = ConstraintItem(target: other, attributes: ConstraintAttributes.none),constant = 0.0 |
height.equalTo(65) | ConstraintConstantTarget | related = ConstraintItem(target: nil, attributes: ConstraintAttributes.none),constant = other |
let editable = ConstraintMakerEditable(self.description)
editable.description.sourceLocation = (file, line)
editable.description.relation = relation
editable.description.related = related
editable.description.constant = constant
3.1.5 ConstraintMakerEditable
**继承类 ConstraintMakerPriortizable **
提供 multipliedBy, dividedBy, offset, inset等一系列方法, 设置常量数据, 填充了ConstraintDescription中如下的数据:
至此,我们可以知道
marqueeLabel.snp.makeConstraints {(make) in
~~** make.left.equalToSuperview()**~~
}
{(make:ConstraintMaker) in
let maker = ConstraintMaker(item: item)
let description = ConstraintDescription(
item: marqueeLabel,
attributes: ConstraintAttributes.left
)
}
这段代码的目标就是生产一个ConstraintDescription,然后将其存储在ConstraintMaker的descriptions数组中中。
marqueeLabel.snp.makeConstraints {(make) in
~~** make.left.equalToSuperview()**~~
make.right.equalToSuperview()
make.top.equalToSuperview()
make.bottom.equalToSuperview()
}
随着block中数据的增多,descriptions逐渐被填满。下面一步就是用descriptions来生成对应的约束了。
3.2、约束描述翻译
涉及类: Constraint
上面我们说的ConstraintDescription,还有最后一个成员变量 constraint: Constraint?,这个变量就是有desc翻译出来的约束,这这个类中,我们最终将descriptions转义成NSLayoutConstraint
3.2.1、Constraint
涉及类:ConstraintItem
Constraint是ConstraintDescription到NSLayoutConstraint中转站,其初始化需要如下参数:
变量名 | 变量类型 | 描述 |
---|---|---|
from | ConstraintItem | 约束将实现在那个view上,这个view上有哪些约束条件 |
to | ConstraintItem | 约束相关的View或者状态 |
relation | ConstraintRelation | 约束关系,equal,less,greater |
multiplier | ConstraintMultiplierTarget | 约束比例 |
constant | ConstraintConstantTarget | 约束值 |
layoutConstraints | LayoutConstraint | 约束值 |
本类所做的就是将约束关系转换成为所要布局的视图和对应的布局视图的位置关系。
然后,统一对每个NSLayoutConstraint执行activate,其本质是调用activateIfNeeded函数.
func activateIfNeeded(updatingExisting: Bool = false) {
let layoutConstraints = self.layoutConstraints
if updatingExisting {
updateExistData()
} else {
NSLayoutConstraint.activate(layoutConstraints)
item.add(constraints: [self])
}
}
到这一步,基本的约束就已经构建成功了。
学到的东西
- 通过协议使用的外观模式
public protocol ConstraintRelatableTarget {
}
extension Int: ConstraintRelatableTarget {
}
......
extension ConstraintInsets: ConstraintRelatableTarget {
}
extension ConstraintItem: ConstraintRelatableTarget {
}
extension ConstraintView: ConstraintRelatableTarget {
}
- 通过别名使用的外观模式
#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
- 不使用库时 AutoLayout的写法
////www.greatytc.com/p/d67395deb694
view.translatesAutoresizingMaskIntoConstraints = false //自动把frame转换成约束,可能导致冲突,需要关闭
marqueeLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: 0).isActive = true
marqueeLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: 0).isActive = true
marqueeLabel.topAnchor.constraint(equalTo: topAnchor, constant: 0).isActive = true
marqueeLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0).isActive = true