SnapKit 是一個(gè)使用 Swift 編寫(xiě)而來(lái)的 AutoLayout 框架, 通過(guò)使用 Snapkit, 我們可以通過(guò)簡(jiǎn)短的代碼完成布局
例如, 我們要一個(gè) label 居中展示
snplabel.snp.makeConstraints { (make) in
make.center.equalTo(self.view.snp.center)
}
如果不用 SnapKit, 我們需要做
rawlabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint(item: rawlabel, attribute: .centerX, relatedBy: .equal, toItem: self.view, attribute: .centerX, multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: rawlabel, attribute: .centerY, relatedBy: .equal, toItem: self.view, attribute: .centerY, multiplier: 1, constant: 20).isActive = true
看起來(lái)很神奇的 SnapKit 是如何實(shí)現(xiàn)的?
分析源碼
我們從最開(kāi)始的 snplabel.snp
開(kāi)始
你也許猜到了, 這個(gè)是通過(guò)給 view 添加一個(gè)擴(kuò)展實(shí)現(xiàn)的
這個(gè)在ConstraintView+Extensions.swift 文件里面, 這個(gè)文件里面有很多廢棄的方法, 為了方便查看, 我們先直接去掉這些廢棄的方法, 去掉之后, 就是這樣的
public extension ConstraintView {
public var snp: ConstraintViewDSL {
return ConstraintViewDSL(view: self)
}
}
擴(kuò)展
你也許注意到, 并不是直接擴(kuò)展的 UIView, 我們來(lái)看看 ConstraintView 的定義
#if os(iOS) || os(tvOS)
public typealias ConstraintView = UIView
#else
public typealias ConstraintView = NSView
#endif
可以看到, SnapKit 為了實(shí)現(xiàn)多平臺(tái)將 ConstraintView 分別定義為 UIView 和 NSView 的別名. 我們這里也為了簡(jiǎn)單起見(jiàn), 不考慮多平臺(tái)適配, 我們將 ConstraintView 都替換為 UIView
public extension UIView {
public var snp: ConstraintViewDSL {
return ConstraintViewDSL(view: self)
}
}
可以看到, snp 最后是生成了一個(gè) ConstraintViewDSL 對(duì)象
ConstraintViewDSL
ConstraintViewDSL 類(lèi)的構(gòu)造函數(shù)很簡(jiǎn)單, 就是將 view 保存起來(lái)
internal init(view: UIView) {
self.view = view
}
而makeConstraints 函數(shù)也是定義如下, 這里看到, 這里只是將傳進(jìn)來(lái)的閉包傳遞給ConstraintMaker 這個(gè)類(lèi)去處理了
public func makeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
ConstraintMaker.makeConstraints(item: self.view, closure: closure)
}
ConstraintMaker
ConstraintMaker.makeConstraints
的實(shí)現(xiàn)如下所示
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)
}
}
從這里可以看到一個(gè)大致流程, 首先是構(gòu)造一個(gè) maker, 然后調(diào)用閉包, 閉包內(nèi)部會(huì)添加一些約束, 接下來(lái)就是獲取這些約束, 最后將約束激活.
這個(gè)類(lèi)的構(gòu)造函數(shù)依舊很簡(jiǎn)單
internal init(item: LayoutConstraintItem) {
self.item = item
self.item.prepare()
}
LayoutConstraintItem
這里出現(xiàn)了一個(gè)新的類(lèi)型 LayoutConstraintItem
, 表示一個(gè)可布局的對(duì)象, 通過(guò)查看定義, 可以看到是一個(gè)協(xié)議, UIView 和 ConstraintLayoutGuide 都實(shí)現(xiàn)了這個(gè)協(xié)議, 內(nèi)部實(shí)現(xiàn)了一些方法, 其中就有這個(gè) prepare
internal func prepare() {
if let view = self as? UIView {
view.translatesAutoresizingMaskIntoConstraints = false
}
}
這一步其實(shí)就是禁用 View 的 AutoresizeMask 轉(zhuǎn)換.
回到最開(kāi)始的閉包, 里面我們寫(xiě)的make.center.equalTo(self.view.snp.center)
通過(guò)上下文我們可以猜到, 我們可以通過(guò)這個(gè)函數(shù)生成一些約束對(duì)象.
首先我們都知道, 每一個(gè)約束, 首先需要添加到一個(gè)對(duì)象上面, 還需要約束的屬性, 關(guān)系(大于, 等于,小于), 如果不是常量類(lèi)型, 還需要另一個(gè)依賴(lài)的對(duì)象, 以及依賴(lài)的屬性, 系數(shù)以及一個(gè)偏移常量.
這里的 make.center
就是說(shuō)添加到當(dāng)前, 并設(shè)置約束屬性為 center, equalTo
, 則是表示關(guān)系為等于, self.view.snp.center, 則表示依賴(lài)的對(duì)象是 self.view, 依賴(lài)的屬性也是 center, 系數(shù)及偏移值這里均沒(méi)有指定, 表示使用默認(rèn)值
那 make.center 這個(gè)是如何實(shí)現(xiàn)的? 通過(guò)查找定義, 可以發(fā)現(xiàn)實(shí)現(xiàn)如下
public var center: ConstraintMakerExtendable {
return self.makeExtendableWithAttributes(.center)
}
這個(gè)只是一個(gè)簡(jiǎn)便方法, 具體的實(shí)現(xiàn)繼續(xù)去查看定義
internal func makeExtendableWithAttributes(_ attributes: ConstraintAttributes) -> ConstraintMakerExtendable {
let description = ConstraintDescription(item: self.item, attributes: attributes)
self.descriptions.append(description)
return ConstraintMakerExtendable(description)
}
可以看到流程為首先根據(jù)約束屬性及需要添加約束的對(duì)象生成一個(gè)描述, 然后將其添加內(nèi)部的一個(gè)數(shù)組, 也就是之前 makeConstraints
中第一個(gè) for 循環(huán)鎖遍歷的數(shù)組, 最后返回一個(gè) ConstraintMakerExtendable 對(duì)象
ConstraintAttributes
首先我們來(lái)看看這個(gè)屬性center
ConstraintAttributes 本身是一個(gè) OptionSet, 里面定義了許多屬性, 例如 left, right, center
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) }
使用 OptionSet 的意義在于, 可以通過(guò)組合操作, 同時(shí)添加多個(gè)屬性, 例如, center 這個(gè)屬性就是由 centerX 和 centerY 復(fù)合而來(lái).
ConstraintDescription
這個(gè)類(lèi)是一個(gè)描述類(lèi), 用于描述一條具體的約束, 里面包含了約束的屬性, 關(guān)系等
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
中的第一個(gè) for 循環(huán), 里面就是去獲取 description.constraint 已達(dá)到最終構(gòu)造約束的目的
ConstraintMakerExtendable
makeExtendableWithAttributes
最后返回的時(shí)候, 返回的是一個(gè)ConstraintMakerExtendable
對(duì)象
這個(gè)類(lèi)的主要目的是為了實(shí)現(xiàn)鏈?zhǔn)降亩鄬傩? 例如, make.center.equalTo(self.view.snp.center)
這一句可以寫(xiě)為, make.centerX.centerY.equalTo(self.view.snp.center)
public class ConstraintMakerExtendable: ConstraintMakerRelatable {
public var left: ConstraintMakerExtendable {
self.description.attributes += .left
return self
}
...
}
ConstraintMakerRelatable
另外, ConstraintMakerExtendable
繼承自 ConstraintMakerRelatable
, 這個(gè)類(lèi)主要是負(fù)責(zé)構(gòu)造一個(gè)關(guān)系, 例如 equalTo
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? 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
只是對(duì)內(nèi)部函數(shù)relatedTo
的一個(gè)簡(jiǎn)單調(diào)用
ConstraintRelatableTarget
這是一個(gè)協(xié)議, 表示一個(gè)可以被依賴(lài)的目標(biāo), 我們?cè)谑謱?xiě) NSLayoutConstraint 的時(shí)候, 依賴(lài)對(duì)象可以為 view, 可以為ConstraintLayoutGuide, 也可以為空, 為空的時(shí)候, 表示使用絕對(duì)值
ConstraintRelatableTarget
是一個(gè)協(xié)議, 分別有 Int, Double, CGPoint等字面值, 也有UIView, ConstraintLayoutGuide , 同時(shí), 也有ConstraintItem, 讓我們可以指定依賴(lài)的具體值, 我們之前的代碼 make.center.equalTo(self.view.snp.center)
中的self.view.snp.center
就是 ConstraintItem
對(duì)象
ConstraintItem
view.snp
返回的是一個(gè) ConstraintViewDSL
, ConstraintViewDSL
是繼承自 ConstraintAttributesDSL
, 而ConstraintAttributesDSL
則是繼承自 ConstraintBasicAttributesDSL
的 ConstraintAttributesDSL
與 ConstraintBasicAttributesDSL
中定義了大量的布局屬性, 如 top, bottom 等
public var center: ConstraintItem {
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.center)
}
...其他均類(lèi)似
可以看到這里面構(gòu)造了一個(gè) ConstraintItem 對(duì)象
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
}
}
這個(gè)類(lèi)也很簡(jiǎn)單, 主要就是保存一下布局的目標(biāo)對(duì)象與目標(biāo)屬性
回到 relateTo 這個(gè)方法中, 這個(gè)方法有4 個(gè)主要分支
第一個(gè)分支就是對(duì)象為 ConstraintItem 的分支
首先使用了 guard 判斷了是否為一個(gè)合法的對(duì)象, 之后就進(jìn)入后續(xù)處理, 而對(duì)于 UIView 和 ConstraintLayoutGuide 則直接將屬性設(shè)置為 none, 而字面值類(lèi)型, 則直接將值保存起來(lái)
獲取了 related 與 constant 之后, 后續(xù)會(huì)使用 description 生成一個(gè) ConstraintMakerEditable, 并在之后, 修改 description , 添加新增的屬性.
ConstraintMakerEditable
ConstraintMakerEditable 這個(gè)類(lèi)主要是設(shè)置Autolayout 中的兩個(gè)常量multiplier 和 constant 與優(yōu)先級(jí)
使用方法如make.center.equalTo(self.view.snp.center).offset(20)
再次回到makeConstraints
通過(guò)上面的若干步驟, 完成了對(duì) ConstraintDescription
的設(shè)置, 現(xiàn)在可以用他來(lái)生成 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 創(chuàng)建過(guò)程很像NSLayoutConstraint
Constraint
這個(gè)類(lèi)主要就是生成和操縱 NSLayoutConstraint
.
構(gòu)造函數(shù)有點(diǎn)長(zhǎng), 下面是去掉一些簡(jiǎn)單的賦值和多平臺(tái)適配后的代碼
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)
}
}
函數(shù)中第一行的self.layoutConstraints = []
使用來(lái)存放所有最后生成的NSLayoutConstraint
后面的兩行是獲取兩個(gè)對(duì)象的約束屬性. 而 layoutFrom
則是約束屬性的起始對(duì)象, 在我們最初那段代碼中, 就表示了snplabel
這個(gè)視圖.
后面則是獲取約束的關(guān)系, 如等于, 大于
主要的代碼都在那個(gè)循環(huán)中, 主要邏輯是遍歷添加在起始對(duì)象上的約束屬性, 然后獲取預(yù)支對(duì)應(yīng)的目標(biāo)對(duì)象及目標(biāo)對(duì)象的約束屬性, 最后生成 LayoutConstraint
其中第一個(gè) if else 分支中在確定目標(biāo)屬性該使用何種值, 通過(guò)分析可以看出, 我們之前那段代碼, 其實(shí)可以將make.center.equalTo(self.view.snp.center)
中直接寫(xiě)為make.center.equalTo(self.view)
(這個(gè)實(shí)現(xiàn)原理在第一個(gè)else 語(yǔ)句中的 else 語(yǔ)句中實(shí)現(xiàn))
后面則是根據(jù)不同的目標(biāo)屬性, 獲取適當(dāng)?shù)钠浦? 以及獲取目標(biāo)對(duì)象.
后面 LayoutConstraint(xxx) 中的 LayoutConstraint 其實(shí)只是一個(gè) NSLayoutConstraint 的子類(lèi), 只是在其中添加了一個(gè)標(biāo)簽與創(chuàng)建者(Constraint) 的引用
activateIfNeeded
makeConstraints
最后一步則是激活, 在 iOS 8 以前, 所有的依賴(lài)屬性, 都必須使用 view.addConstraint(xxx)
方法將依賴(lài)激活, iOS 8 后, 則直接將依賴(lài)激活即可生效.
activateIfNeeded 則是將依賴(lài)激活使其生效