寫在前面
經(jīng)過激烈的思想斗爭烫幕,筆者從Android開發(fā)轉(zhuǎn)到了iOS開發(fā),發(fā)現(xiàn)了兩者很多的共同之處,這里就不在贅述庐镐;不過最大的不適應(yīng)體現(xiàn)在UI方面,Android的布局編寫和預(yù)覽更舒適变逃。
萬般無奈之下必逆,接觸到了SnapKit,一個用Swift編寫的AutoLayout框架揽乱,極大程度上簡化了純布局的代碼名眉。
分析源碼
本文只探究makeConstraints
的過程,也就是停留在閉包之外凰棉。
ConstraintView
SnapKit的最基本用法:
view.snp.makeConstraints { (make) in
}
首先view.snp
很容易讓人想到是使用了擴(kuò)展损拢,但并不是直接對UIView
的擴(kuò)展,而是要引入一個新的概念ConstraintView
撒犀,具體情況在ConstraintView.swift
中體現(xiàn):
#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
這就是該文件所有的代碼了探橱,可以看到申屹,通過判斷當(dāng)前系統(tǒng)做了兩件事:
- 包的導(dǎo)入:如果當(dāng)前系統(tǒng)是
iOS
或者tvOS
,那么導(dǎo)入UIKit
隧膏,否則導(dǎo)入AppKit
- 類的重命名:如果當(dāng)前系統(tǒng)是
iOS
或者tvOS
哗讥,那么將UIView
重命名為ConstraintView
,否則將NSView
重命名為ConstraintView
胞枕。其中typealias
用于為已存在的類重新命名杆煞,提高代碼的可讀性。
總而言之腐泻,ConstraintView
是為了適配多平臺而定義的UIView
或NSView
的別稱决乎。
extension ConstraintView
緊接上文,view.snp
是對ConstraintView
的擴(kuò)展派桩,在ConstraintView+Extensions.swift
中返回:
#if os(iOS) || os(tvOS)
import UIKit
#else
import AppKit
#endif
public extension ConstraintView {
// 此處略去很多廢棄的方法
public var snp: ConstraintViewDSL {
return ConstraintViewDSL(view: self)
}
}
此處省略了該文件中很多被廢棄的方法构诚,只看最關(guān)鍵的變量snp
,此處返回了一個新的對象ConstraintViewDSL
铆惑,并以自己范嘱,一個ConstraintView
作為參數(shù)。
注意:在SnapKit中员魏,幾乎所有文件開頭都有關(guān)于導(dǎo)入UIKit
還是AppKit
的判斷丑蛤,之后就不再展示這段重復(fù)的代碼。
ConstraintViewDSL
接下來jump到ConstraintViewDSL.swift
文件中撕阎,這里只展示它的一個最關(guān)鍵方法:
public struct ConstraintViewDSL: ConstraintAttributesDSL {
public func makeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
ConstraintMaker.makeConstraints(item: self.view, closure: closure)
}
internal let view: ConstraintView
internal init(view: ConstraintView) {
self.view = view
}
}
首先可以看到ConstraintViewDSL
是一個結(jié)構(gòu)體受裹,實現(xiàn)了ConstraintAttributesDSL
接口,構(gòu)造函數(shù)也非常簡單虏束,只接收一個ConstraintView
并保存起來棉饶;另外,view.snp.makeConstraints
也只是把保存的ConstraintView
镇匀,連同傳遞進(jìn)來的閉包一起交給ConstraintMaker
處理砰盐。
除了makeConstraints
方法,還有remakeConstraints
坑律、updateConstraints
、removeConstraints
等方法囊骤,因為都是交給ConstraintMaker
處理晃择,所以不再贅述。
ConstraintMaker
ConstraintMaker.swift
文件中:
public class ConstraintMaker {
private let item: LayoutConstraintItem
private var descriptions = [ConstraintDescription]()
internal init(item: LayoutConstraintItem) {
self.item = item
self.item.prepare()
}
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
}
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)
}
}
}
ConstraintMaker
是一個類也物,從上面展示的代碼可以知道創(chuàng)建約束的基本流程:首先makeConstraints
調(diào)用prepareConstraints
宫屠,構(gòu)造一個maker
,然后由閉包調(diào)用這個maker
滑蚯,遍歷maker
的descriptions
浪蹂,將獲取的約束添加到一個約束數(shù)組constraints
中抵栈,然后prepareConstraints
執(zhí)行完畢并將約束返回這個constraints
,makeConstraints
繼續(xù)執(zhí)行坤次,獲取這些約束古劲,然后激活。
構(gòu)造maker
時缰猴,傳入構(gòu)造函數(shù)的item
應(yīng)為保存在ConstraintViewDSL
中的ConstraintView
产艾,但在init
聲明中變成了LayoutConstraintItem
?
LayoutConstraintItem
LayoutConstraintItem.swift
:
public protocol LayoutConstraintItem: class {
}
extension ConstraintView : LayoutConstraintItem {
}
可以看到這是一個協(xié)議滑绒,并且ConstraintView
實現(xiàn)了它闷堡,協(xié)議中也實現(xiàn)了一些方法,其中就包括prepare
:
extension LayoutConstraintItem {
internal func prepare() {
if let view = self as? ConstraintView {
view.translatesAutoresizingMaskIntoConstraints = false
}
}
}
prepare
方法禁用了從AutoresizingMask
到Constraints
的自動轉(zhuǎn)換疑故,即translatesAutoresizingMaskIntoConstraints
可以把 frame 杠览,bouds,center 方式布局的視圖自動轉(zhuǎn)化為約束形式纵势,轉(zhuǎn)化的結(jié)果就是自動添加需要的約束踱阿;而此時我們需要自己添加約束,必然會產(chǎn)生沖突吨悍,所以直接指定這個視圖不去使用約束布局扫茅。
中途休息一下
到目前為止,我們知道了調(diào)用view.snp.makeConstraints
時育瓜,這個view經(jīng)過一系列轉(zhuǎn)運葫隙,最終禁用了自己的約束布局,而這個過程僅僅是prepareConstraints
方法的第一行躏仇,也就是只調(diào)用了ConstraintMaker
的構(gòu)造函數(shù)恋脚,接下來繼續(xù)分析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
}
構(gòu)造maker
之后焰手,先是執(zhí)行了閉包的內(nèi)容(不在本文討論范圍內(nèi))糟描,緊接著創(chuàng)建了一個包含Constraint
的數(shù)組constraints
;然后遍歷包含了ConstraintDescription
類型的descriptions
數(shù)組(該數(shù)組是maker
成員變量书妻,具體可以往上翻翻)船响,并試圖將每個description
中包含的constraint
添加到constraints
數(shù)組中,最后返回該數(shù)組躲履。
ConstraintDescription
ConstraintDescription.swift
:
public class ConstraintDescription {
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
)
}()
}
此處略去了很多成員變量见间,簡單來說,ConstraintDescription
內(nèi)部持有一個Constraint
變量工猜,需要時可以利用自己的成員變量構(gòu)造出一個Constraint
并返回米诉。
Constraint
Constraint.swift
中,關(guān)鍵代碼在構(gòu)造函數(shù)篷帅,略去成員變量和方法史侣,以及構(gòu)造函數(shù)中關(guān)于多平臺的適配之后拴泌,內(nè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)
}
}
首先創(chuàng)建layoutConstraints
來保存最后生成的所有LayoutConstraint
(繼承自NSLayoutConstraint
),然后獲取該約束的起始對象的約束屬性layoutFromAttributes
和目標(biāo)對象的約束屬性layoutToAttributes
惊橱。接下來的主要邏輯就在循環(huán)體內(nèi)蚪腐,通過遍歷起始對象的約束屬性,然后獲取目標(biāo)對象的約束屬性李皇,最終創(chuàng)建一條新的約束削茁。
至此,我們可以認(rèn)為prepareConstraints
執(zhí)行完畢掉房,makeConstraints
已經(jīng)獲取到了所有需要的約束茧跋,接下來要執(zhí)行最后一步:激活約束
activateIfNeeded
這是Constraint.swift
中的一個方法:
internal func activateIfNeeded(updatingExisting: Bool = false) {
guard let item = self.from.layoutConstraintItem else {
print("WARNING: SnapKit failed to get from item from constraint. Activate will be a no-op.")
return
}
let layoutConstraints = self.layoutConstraints
if updatingExisting {
var existingLayoutConstraints: [LayoutConstraint] = []
for constraint in item.constraints {
existingLayoutConstraints += constraint.layoutConstraints
}
for layoutConstraint in layoutConstraints {
let existingLayoutConstraint = existingLayoutConstraints.first { $0 == layoutConstraint }
guard let updateLayoutConstraint = existingLayoutConstraint else {
fatalError("Updated constraint could not find existing matching constraint to update: \(layoutConstraint)")
}
let updateLayoutAttribute = (updateLayoutConstraint.secondAttribute == .notAnAttribute) ? updateLayoutConstraint.firstAttribute : updateLayoutConstraint.secondAttribute
updateLayoutConstraint.constant = self.constant.constraintConstantTargetValueFor(layoutAttribute: updateLayoutAttribute)
}
} else {
NSLayoutConstraint.activate(layoutConstraints)
item.add(constraints: [self])
}
}
這里首先獲取了起始目標(biāo)item
,類型為LayoutConstraintItem
卓囚,有變量constraintsSet
來保存所有的約束瘾杭;
然后獲取了自己的layoutConstraints
數(shù)組,Constraint
不單指一個約束哪亿,而是layoutConstraints
中所有約束的集合粥烁,或者說是snp.makeConstraints
過程中的一個集合;
最后通過NSLayoutConstraint.activate
激活了整個layoutConstraints
數(shù)組中的約束蝇棉,并且將這些約束添加到了起始目標(biāo)的約束集合中保存起來讨阻。