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