简介
什么是Snapkit
- SnapKit是一个使用 Swift 编写而来的AutoLayout框架,通过使用Snapkit,我们可以通过简短的代码完成布局,如下所示:
原生布局
contentView.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
addConstraint(NSLayoutConstraint(item: imageView,
attribute: .leading,
relatedBy: .equal,
toItem: contentView,
attribute: .leading,
multiplier: 1,
constant: 0))
addConstraint(NSLayoutConstraint(item: imageView,
attribute: .top,
relatedBy: .equal,
toItem: contentView,
attribute: .top,
multiplier: 1,
constant: 0))
addConstraint(NSLayoutConstraint(item: imageView,
attribute: .trailing,
relatedBy: .equal,
toItem: contentView,
attribute: .trailing,
multiplier: 1,
constant: 0))
addConstraint(NSLayoutConstraint(item: imageView,
attribute: .bottom,
relatedBy: .equal,
toItem: contentView,
attribute: .bottom,
multiplier: 1,
constant: 0))
SnapKit布局:
contentView.addSubview(imageView)
imageView.snp.makeConstraints { make in
make.edges.equalTo(contentView)
}
- DSL(Domain specific Language)特定领域语言
DSL是为了解决某些特定场景下的任务而专门设计的语言。如果能把一些设计师产出的长宽、色值、文字、居中、距上等设计元数据(设计的标注信息等),以一种约定的简洁的语言规则(即DSL)输入给程序代码,由程序和代码自动的分析和处理,从而生成真正的界面开发代码setFrame,setTitle,setColor,addSubview,这样就可以大幅度的减少代码量与工作量,程序员来写这种简洁的语法规则会更快更高效,甚至可以把这种简洁的语法规则教会设计师,让设计师有能力直接写出DSL,然后输入给底层程序,这样界面就自然完成。
注意事项
- 使用SnapKit前,一定要先将子控件添加到父视图中,否则会直接崩溃!
parentView.addSubview(subview)
- leading和left、trailing和right
其实在目前国内App中使用leading与left,trailing与right在正常情况下是等价的,这是因为国内的阅读习惯是从左到右的,不过如果你的App需要在阿拉伯国家上架,他们的布局是从右至左时(比如阿拉伯文) 则会对调。
建议使用leading和trailing,便于App国际化。
使用教程
- 较为简单,api也不多,不多描述了
源码解析
详细解析
- snp
lable.snp通过给view加扩展实现的
public extension ConstraintView {
public var snp: ConstraintViewDSL {
return ConstraintViewDSL(view: self
}
}
snp 最后是生成了一个 ConstraintViewDSL 对象
- ConstraintView的定义
if os(iOS) || os(tvOS)
public typealias ConstraintView = UIView
#else public
typealias ConstraintView = NSView
#endif
这里tvOS是基于 iOS的操作系统,tvOS 是专门为第四代 Apple TV设计的操作系统。
- ConstraintViewDSL
internal init(view: ConstraintView) {
self.view = view
}
ConstraintViewDSL 类的构造函数,就是将 view 保存起来
public func makeConstraints(_ closure:
(_ make: ConstraintMaker) -> Void){
ConstraintMaker.makeConstraints(item:self.view, closure: closure)
}
makeConstraints 函数将传进来的闭包传递给ConstraintMaker 这个类去处理了
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)
}
}
该方法主要调用了被接受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,然后调用闭包,闭包内部会添加一些约束,接下来就是获取这些约束, 最后将约束激活。
闭包就是能够读取其他函数内部变量的函数。例如在程序中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“在本质上,闭包是将函数内部和函数外部连接起来的桥梁。
internal init(item: LayoutConstraintItem) {
self.item = item
self.item.prepare()
}
这是ConstraintMaker的构造函数,这里出现了一个新的类型LayoutConstraintItem,表示一个可布局的对象。
public protocol LayoutConstraintItem: class {
}
可以看到这是一个协议
extension ConstraintLayoutGuide : LayoutConstraintItem {
}
extension ConstraintView : LayoutConstraintItem {
}
ConstraintView 和 ConstraintLayoutGuide 都实现LayoutConstraintItem这个协议。
extension LayoutConstraintItem {
internal func prepare() {
if let view = self as? ConstraintView {
view.translatesAutoresizingMaskIntoConstraints = false
}
}
}
该协议实现了一些方法,包含prepare方法。这一步其实就是禁用 View 的 AutoresizeMask。
回到开始的闭包,里面我们写的make.center.equalTo(self.view.snp.center)可以通过这个函数生成一些约束对象。首先我们都知道, 每一个约束, 首先需要添加到一个对象上面, 还需要约束的属性,关系大于、等于、小于,如果不是常量类型,还需要另一个依赖的对象,以及依赖的属性,系数以及一个偏移常量。
这里的 make.center就是说添加到当前,并设置约束属性center,equalTo,则是表示关系为等于,self.view.snp.center则表示依赖的对象是 self.view,依赖的属性也是 center,系数及偏移值这里均没有指定,表示使用默认值。
public var center: ConstraintMakerExtendable {
return self.makeExtendableWithAttributes(.center)
}
这个只是一个简便方法, 具体的实现继续去查看定义
internal func makeExtendableWithAttributes(_ attributes: ConstraintAttributes) -> ConstraintMakerExtendable {
let description = ConstraintDescription(item: self.item, attributes: attributes)
self.descriptions.append(description)
return ConstraintMakerExtendable(description)
}
流程为首先根据约束属性及需要添加约束的对象生成一个描述,然后将其添加内部的一个数组,也就是之前 makeConstraints中第一个 for 循环锁遍历的数组,最后返回一个 ConstraintMakerExtendable 对象。
- ConstraintAttributes
internal struct ConstraintAttributes : OptionSet, ExpressibleByIntegerLiteral {
}
ConstraintAttributes 本身是一个 OptionSet
public protocol OptionSet : RawRepresentable, SetAlgebra {
}
extension RawRepresentable where Self : Encodable, Self.RawValue == String {
public func encode(to encoder: Encoder) throws
}
extension RawRepresentable where Self : Decodable, Self.RawValue == String {
public init(from decoder: Decoder) throws
}
初始化,成为统一可操作的类型。
internal struct ConstraintAttributes : OptionSet {
internal private(set) var rawValue: UInt internal init(rawValue: UInt) { self.rawValue = rawValue
}
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 center: ConstraintAttributes {
return self.init(768)
}
}
ConstraintAttributes 本身是一个 OptionSet,里面定义了许多属性, 例如 left, right, center使用 OptionSet 的意义在于,可以通过组合操作,同时添加多个属性,例如,center这个属性就是由 centerX 和 centerY 复合而来。
public class ConstraintDescription {
internal let item: LayoutConstraintItem
internal var attributes: ConstraintAttributes
internal var relation: ConstraintRelation? = nil
internal var sourceLocation: (String, UInt)? = nil
internal var label: String? = nil
internal var related: ConstraintItem? = nil
internal var multiplier: ConstraintMultiplierTarget = 1.0
internal var constant: ConstraintConstantTarget = 0.0
internal var priority: ConstraintPriorityTarget = 1000.0
internal lazy var constraint: Constraint? =
...
internal init(item: LayoutConstraintItem, attributes: ConstraintAttributes){
self.item = item
self.attributes = attributes
}
这个类是一个描述类,用于描述一条具体的约束,里面包含了约束的属性,关系等回到ConstraintMaker.makeConstraints 中的第一个 for 循环,里面就是去获取description.constraint 已达到最终构造约束的目的。
public class ConstraintMakerExtendable: ConstraintMakerRelatable {
public var left: ConstraintMakerExtendable {
self.description.attributes += .left
return self
}
...
}
makeExtendableWithAttributes最后返回的时候, 返回的是一ConstraintMakerExtendable对象。这个类的主要目的是为了实现链式的多属性,例如,make.center.equalTo(self.view.snp.center)
这一句可以写为,make.centerX.centerY.equalTo(self.view.snp.center)
public func equalTo(_ other: ConstraintRelatableTarget, _ file: String = #file, _ line: UInt = #line) -> ConstraintMakerEditable {
return self.relatedTo(other, relation: .equal, file: file, line: line)
}
ConstraintMakerExtendable 继承自 ConstraintMakerRelatable,这个类主要是负责构造一个关系,例如 equalTo
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? UIView {
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
} // equalTo 只是对内部函数relatedTo 的一个简单调用
public protocol ConstraintRelatableTarget {
}
extension Int: ConstraintRelatableTarget {
}
extension UInt: ConstraintRelatableTarget {
}
extension Float: ConstraintRelatableTarget {
}
extension ConstraintItem: ConstraintRelatableTarget {
}
extension ConstraintView: ConstraintRelatableTarget {
}
ConstraintRelatableTarget是一个协议,表示一个可以被依赖的目标,我们在手写 NSLayoutConstraint 的时候,
依赖对象可以为 view,可以为ConstraintLayoutGuide,也可以为空,为空的时候,表示使用绝对值,该协议分别有 Int、 Double、CGPoint等字面值,也有UIView, ConstraintLayoutGuide,同时,也有ConstraintItem,让我们可以指定依赖的具体值, 我们之前的代码 make.center.equalTo(self.view.snp.center)中的self.view.snp.center就是 ConstraintItem对象。
- ConstraintItem
view.snp返回的是一个 ConstraintViewDSL,ConstraintViewDSL是继承自 ConstraintAttributesDSL,而ConstraintAttributesDSL则是继承自 ConstraintBasicAttributesDSL的ConstraintAttributesDSL与 ConstraintBasicAttributesDSL中定义了大量的布局属性,如 top, bottom 等
public var center: ConstraintItem { return ConstraintItem(target: self.target, attributes: ConstraintAttributes.center) } …
其他均类似。可以看到这里面构造了一个 ConstraintItem 对象:
public final class ConstraintItem {
internal weak var target: AnyObject?
internal let attributes: ConstraintAttributes
internal init(target: AnyObject?, attributes: ConstraintAttributes) {
self.target = target
self.attributes = attributes
}
internal var layoutConstraintItem: LayoutConstraintItem? {
return self.target as? LayoutConstraintItem
}
}
- ConstraintMakerEditable
ConstraintMakerEditable 这个类主要是设置Autolayout 中的两个常量multiplier 和 constant 与优先级,使用方法如make.center.equalTo(self.view.snp.center).offset(20)
再次回到makeConstraints,通过上面的若干步骤,完成了对 ConstraintDescription的设置,现在可以用他来生成 Constraint了,生成的部分在ConstraintDescription 的 constraint 属性里面
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 )
}()
Constraint 创建过程很像NSLayoutConstraint
Constraint这个类主要就是生成和操纵 NSLayoutConstraint。构造函数有点长,下面是去掉一些简单的赋值和多平台适配后的代码
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
……
函数中第一行的self.layoutConstraints = []使用来存放所有最后生成的NSLayoutConstraint
后面的两行是获取两个对象的约束属性。而 layoutFrom则是约束属性的起始对象,在我们最初那段代码中,就表示了snplabel这个视图。
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)
}
}
后面则是获取约束的关系, 如等于, 大于。主要的代码都在那个循环中,主要逻辑是遍历添加在起始对象上的约束属性,然后获取预支对应的目标对象及目标对象的约束属性,最后生成LayoutConstraint
其中第一个 if else 分支中在确定目标属性该使用何种值, 通过分析可以看出, 我们之前那段代码, 其实可以将make.center.equalTo(self.view.snp.center)
中直接写为make.center.equalTo(self.view)
后面则是根据不同的目标属性,获取适当的偏移值。以及获取目标对象。
后面 LayoutConstraint(xxx) 中的 LayoutConstraint 其实只是一个NSLayoutConstraint 的子类,只是在其中添加了一个标签与创建者(Constraint) 的引用
- activateIfNeeded
makeConstraints最后一步则是激活, 在 iOS 8 以前, 所有的依赖属性, 都必须使用 view.addConstraint(xxx)方法将依赖激活, iOS 8 后, 则直接将依赖激活即可生效。activateIfNeeded 则是将依赖激活使其生效