SnapKit是基于NSLayoutConstraint封裝的一個(gè)輕量級(jí)的布局框架.區(qū)別于iOS9.0中蘋(píng)果引入的系統(tǒng)框架NSLayoutAnchor。其實(shí)NSLayoutAnchor是一個(gè)工廠類,類似NSNumber這樣的設(shè)計(jì)思想.
開(kāi)始
當(dāng)我們開(kāi)始寫(xiě)約束的時(shí)候臭胜,一般都從
view.snp.makeConstraints()方法開(kāi)始,通過(guò)點(diǎn)擊snp我們進(jìn)到里面看轴术,發(fā)現(xiàn)它是下面這個(gè)樣子:
// ConstraintView 實(shí)際上就是UIView
extension ConstraintView {
public var snp: ConstraintViewDSL {
return ConstraintViewDSL(view: self)
}
}
我們現(xiàn)在知道了實(shí)現(xiàn)約束的功能其實(shí)跟這個(gè)ConstraintViewDSL類有很大關(guān)系,這里也是我們理解snapKit庫(kù)如何實(shí)現(xiàn)開(kāi)始的地方钦无,下面開(kāi)始詳細(xì)介紹這個(gè)類逗栽。
ConstraintViewDSL
查看源碼我們發(fā)現(xiàn)這個(gè)類遵守了一個(gè)ConstraintAttributesDSL協(xié)議。這個(gè)協(xié)議里面沒(méi)有定義屬性和方法失暂,默認(rèn)實(shí)現(xiàn)了一些功能:
protocol ConstraintAttributesDSL: ConstraintBasicAttributesDSL { }
extension ConstraintAttributesDSL {
public var top: ConstraintItem {
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.top)
}
public var bottom: ConstraintItem {
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.bottom)
}
...
}
現(xiàn)在我們還不知道ConstraintItem是做什么的彼宠,不過(guò)沒(méi)關(guān)系鳄虱,下面會(huì)詳細(xì)說(shuō)到。接著回到ConstraintViewDSL類里面凭峡,可以看到有一些我們常用到的方法:
public func makeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
ConstraintMaker.makeConstraints(item: self.view, closure: closure)
}
public func updateConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
ConstraintMaker.updateConstraints(item: self.view, closure: closure)
}
講到這里我們可以了解到拙已,項(xiàng)目中常用到的view.snp.makeConstraints() 方法和 view.snp.bottom 其實(shí)都是在ConstraintViewDSL類里面定義的。那現(xiàn)在關(guān)于ConstraintViewDSL類我們就先講到這吧摧冀,下面我們重點(diǎn)去了解ConstraintMaker和ConstraintItem是干什么的倍踪。
ConstraintMaker
寫(xiě)一段最簡(jiǎn)單的代碼,讓ConstraintMaker和我們見(jiàn)見(jiàn)面索昂。
view.snp.makeConstraints({make: ConstraintMaker in
make.top.equalTo(20)
})
這是我們有疑問(wèn)建车,為什么作者不直接在ConstraintMaker上面寫(xiě)約束呢,給我們ConstraintViewDSL類有什么用呢椒惨,下面我們將這種框架核心簡(jiǎn)單來(lái)實(shí)現(xiàn)下缤至,站在作者的角度去看待問(wèn)題:
- 首先我們自己來(lái)定義一個(gè)ConstraintMaker類,內(nèi)部實(shí)現(xiàn)如下:
class ConstraintMaker {
var item: UIView
var descriptions = [Constraint]()
init(item: UIView) {
self.item = item
}
static func prepareConstraints(item: UIView, closure: (_ make: ConstraintMaker) -> Void) -> [Constraint] {
let maker = ConstraintMaker(item: item)
closure(maker)
return maker.descriptions
}
static func makeConstraints(item: UIView, closure: (_ make: ConstraintMaker) -> Void) {
let constraints = prepareConstraints(item: item, closure: closure)
for constraint in constraints {
constraint.active()
}
}
}
extension ConstraintMaker {
var bottom: ConstraintMakerExtendable {
// 這里先簡(jiǎn)單實(shí)現(xiàn)下ConstraintAttributes
let attr = ConstraintAttributes()
return makeExtendableWithAttributes(attr)
}
var top: ConstraintMakerExtendable {
// 這里先簡(jiǎn)單實(shí)現(xiàn)下ConstraintAttributes
let attr = ConstraintAttributes()
return makeExtendableWithAttributes(attr)
}
private func makeExtendableWithAttributes(_ attributes: ConstraintAttributes) -> ConstraintMakerExtendable {
// 這里存在著將ConstraintAttributes對(duì)象轉(zhuǎn)化成了Constraint對(duì)象
let constraint = Constraint()
descriptions.append(constraint)
return ConstraintMakerExtendable()
}
}
這些就是snapKit實(shí)現(xiàn)約束的核心方法了康谆,為了不報(bào)錯(cuò)领斥,這里把剩下的幾個(gè)輔助的類也定義了,它們內(nèi)部只實(shí)現(xiàn)了一些簡(jiǎn)單的方法:
class Constraint {
func active() { print("開(kāi)始布局啦") }
}
class ConstraintMakerExtendable {
func calc() { print("計(jì)算約束") }
}
class ConstraintAttributes {
static var top = ConstraintAttributes()
static var bottom = ConstraintAttributes()
}
接著我們還需要定義一個(gè)ConstraintViewDSL類沃暗,用來(lái)承載具體的約束:
class ConstraintViewDSL {
internal let view: UIView
internal init(view: UIView) {
self.view = view
}
public func makeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
ConstraintMaker.makeConstraints(item: self.view, closure: closure)
}
}
extension UIView {
var snp: ConstraintViewDSL {
return ConstraintViewDSL(view: self)
}
}
大功告成月洛,現(xiàn)在我們也可以自己寫(xiě)一個(gè)約束庫(kù)了,在外面調(diào)用:
let view1 = UIView()
view1.snp.makeConstraints { (make) in
let bottom: ConstraintMakerExtendable = make.bottom
bottom.calc()
let top: ConstraintMakerExtendable = make.top
top.calc()
}
打印如下:
計(jì)算約束
計(jì)算約束
開(kāi)始布局啦
開(kāi)始布局啦
回到ConstraintMaker中繼續(xù)講
SnapKit的作者寫(xiě)了很多的類孽锥,當(dāng)我們第一眼看到這么多類嚼黔,會(huì)覺(jué)得無(wú)從下手,不知道從哪開(kāi)始閱讀忱叭,下面我大概整理了一下隔崎,約束的過(guò)程今艺。
// 一條完整的約束一般是這樣的:
make.top.left.equalTo().offset().priority()
//鏈?zhǔn)秸{(diào)用的關(guān)系如下:
make: ConstraintMaker
.top: ConstraintMakerExtendable
.left: ConstraintMakerExtendable(繼承自下面的ConstraintMakerRelatable)
.equalTo: ConstraintMakerRelatable
.offset: ConstraintMakerEditable(繼承自下面的ConstraintMakerPriortizable)
.priority: ConstraintMakerPriortizable
// .offset韵丑、.priority類繼承關(guān)系
ConstraintMakerEditable -> ConstraintMakerPriortizable -> ConstraintMakerFinalizable
// .top、.left虚缎、.equalTo類繼承關(guān)系
ConstraintMakerExtendable -> ConstraintMakerRelatable
通過(guò)閱讀源碼可以看到撵彻,調(diào)用view.snp.makeConstraints()方法,實(shí)際上內(nèi)部是先調(diào)用prepareConstraints方法將約束準(zhǔn)備好实牡,在調(diào)用activate()將約束添加到視圖上:
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)
}
}
activeate方法作為添加約束內(nèi)部實(shí)際調(diào)用的是:
NSLayoutConstraint.activate(layoutConstraints)
這個(gè)方法我們比較熟悉陌僵,是系統(tǒng)添加約束的方法,只是activate方法做了一層封裝创坞,當(dāng)然這個(gè)方法除了單純添加約束也可作為更新約束使用碗短,下面會(huì)詳細(xì)講解到。
上面這種圖很形象的表示了約束執(zhí)行的過(guò)程题涨,在makeExtendableWithAttributes方法中偎谁,maker對(duì)象調(diào)用它的.bottom方法(類型為ConstraintMakerExtendable)將約束添加到descriptions數(shù)組中总滩,返回ConstraintMakerExtendable類型進(jìn)行下一次的鏈?zhǔn)秸{(diào)用,然后獲取準(zhǔn)備生產(chǎn)的Array<Constraint>,最后進(jìn)行加工:
// maker對(duì)象調(diào)用makeConstraints方法巡雨,開(kāi)始加工
func makeConstraints() {
// 獲得半成品
let constraints = prepareConstraints(item: item, closure: closure)
// 開(kāi)始加工
for constraint in constraints {
constraint.activateIfNeeded(updatingExisting: false)
}
}
我們繼續(xù)看ConstraintMaker中的加工的機(jī)器:
func makeExtendableWithAttributes(_ attributes: ConstraintAttributes) -> ConstraintMakerExtendable {
// 一條完整的約束描述類 類似 make.top.equal(10)
let description = ConstraintDescription(item: self.item, attributes: attributes)
//將單條約束添加到數(shù)組中
self.descriptions.append(description)
// 它可以實(shí)現(xiàn)多個(gè)屬性鏈?zhǔn)讲僮?有了它 就可以實(shí)現(xiàn) make.width.height 這種特性
// 其中width是ConstraintMaker的屬性 height是ConstraintMakerExtendable的屬性
// 它們都是ConstraintMakerExtendable類型
return ConstraintMakerExtendable(description)
}
有了這個(gè)ConstraintMakerExtendable類就可以通過(guò)鏈?zhǔn)秸{(diào)用比如.width方法闰渔,.height方法添加一些約束,來(lái)一步步完善ConstraintDescription類
ConstraintDescription可以看成是Constraint腳手架铐望,在一步步添加約束時(shí)操作的都是ConstraintDescription類冈涧,等將所有約束添加到數(shù)組中,準(zhǔn)備下一步生產(chǎn)時(shí)正蛙,會(huì)拿到它內(nèi)部的constraint屬性(Constraint類型)進(jìn)行操作督弓。
題外話,既然makeConstraints()方法內(nèi)部執(zhí)行了兩步操作跟畅,那我們就可以利用這個(gè)特性咽筋,在視圖有多種布局的時(shí)候,可以用到prepareConstraints方法徊件,將布局提前裝載好奸攻,然后根據(jù)狀態(tài)執(zhí)行不同顯示效果,代碼如下:
let v1 = View()
let v2 = View()
self.container.addSubview(v1)
self.container.addSubview(v2)
let constraints = v1.snp.prepareConstraints { (make) -> Void in
make.edges.equalTo(v2)
return
}
//打印 self.container.snp_constraints.count == 0,
for constraint in constraints {
constraint.activate()
}
//打印 self.container.snp_constraints.count == 4,
for constraint in constraints {
constraint.deactivate()
}
//打印 self.container.snp_constraints.count == 0,
再來(lái)講講 .equalTo()
其實(shí)和它類似的方法有很多包括:.equalToSuperview(), .lessThanOrEqualTo(), .lessThanOrEqualToSuperview(), .greaterThanOrEqualTo(), .greaterThanOrEqualToSuperview()實(shí)現(xiàn)的功能類似虱痕。相同點(diǎn)在是它內(nèi)部調(diào)用的是同一個(gè)方法:
func relatedTo(_ other: ConstraintRelatableTarget, relation: ConstraintRelation, file: String, line: UInt) -> ConstraintMakerEditable {
這個(gè)方法返回一個(gè)ConstraintMakerEditable類型睹耐,用于對(duì)約束添加附加的操作(offset偏移量,priority優(yōu)先級(jí))部翘。進(jìn)入到這個(gè)方法內(nèi)部硝训,我們將核心代碼提取出來(lái):
func relatedTo(_ other: ConstraintRelatableTarget) -> ConstraintMakerEditable {
if let other = other as? ConstraintItem {
//這里處理參數(shù)類似于equalTo(view.snp.bottom)
} else if let other = other as? ConstraintView {
//這里處理參數(shù)類似于equalTo(view)
} else if let other = other as? ConstraintConstantTarget {
//這里處理參數(shù)類似于equalTo(50)
} else if let other = other as? ConstraintLayoutGuide {
//這里處理參數(shù)類似于equalTo(layoutGuide)
}
}
let v1 = View()
let g1 = UILayoutGuide()
self.container.addSubview(v1)
self.container.addLayoutGuide(g1)
v1.snp.makeConstraints { (make) -> Void in
make.top.equalTo(g1).offset(50)
make.left.equalTo(g1.snp.top).offset(50)
}
如何實(shí)現(xiàn)make.top.equalTo(view)和make.top.equalTo(view.snp.top)效果一樣?
通過(guò)查看ConstraintMakerRelatable類下面的relatedTo()方法,我們可以看到在傳入不同類型的參數(shù)時(shí)(view和view.snp.top分別為UIView類型和ConstraintItem類型)新思,方法內(nèi)部經(jīng)過(guò)處理窖梁,全部轉(zhuǎn)化成了ConstraintItem處理,這時(shí)夹囚,我們猜想當(dāng)參數(shù)類型是UIView時(shí)纵刘,是否自動(dòng)轉(zhuǎn)為了ConstraintItem類型,帶著這個(gè)疑問(wèn)我們接著看:
在relatedTo方法中關(guān)于視圖的判斷邏輯是這樣子的:
if let other = other as? ConstraintView {
related = ConstraintItem(target: other, attributes: ConstraintAttributes.none)
}
如果傳入的是視圖對(duì)象荸哟,則ConstraintItem對(duì)象的attributes就置為了.none,應(yīng)該和這個(gè)有關(guān)假哎,我們?cè)趤?lái)了解一下ConstraintAttributes是做什么的?
ConstraintAttributes
這是一個(gè)結(jié)構(gòu)體類型鞍历,它遵守兩個(gè)協(xié)議舵抹,通過(guò)這個(gè)字面量協(xié)議和多選協(xié)議,完成以組合的形式加入約束劣砍,最后通過(guò)layoutAttributes數(shù)組惧蛹,對(duì)接到NSLayoutConstraint.Attribute中,下面我們來(lái)實(shí)現(xiàn)一個(gè):
enum LayoutAttribute: CustomDebugStringConvertible {
case left
case right
case top
case width
var debugDescription: String {
switch self {
case .left: return "左邊"
case .right: return "右邊"
case .top: return "上邊"
case .width: return "寬度"
}
}
}
struct ConstraintAttributes: OptionSet, ExpressibleByIntegerLiteral {
typealias IntegerLiteralType = UInt
var rawValue: UInt
init(rawValue: UInt) {
self.rawValue = rawValue
}
init(_ rawValue: UInt) {
self.init(rawValue: rawValue)
}
init(integerLiteral value: IntegerLiteralType) {
self.init(value)
}
static var none: ConstraintAttributes { return 0 }
static var left: ConstraintAttributes { return 1 }
static var right: ConstraintAttributes { return 2 }
}
extension ConstraintAttributes {
var layoutAttributes:[LayoutAttribute] {
var attrs = [LayoutAttribute]()
if contains(ConstraintAttributes.left) { attrs.append(.left) }
if contains(ConstraintAttributes.right) { attrs.append(.right) }
if contains(ConstraintAttributes.none) { /*什么都不做*/ }
return attrs
}
}
在外面調(diào)用:
let attributes: ConstraintAttributes = [.left, .right]
let description = attributes.layoutAttributes.map{ $0.debugDescription }
print(description)
//["左邊", "右邊"]
另外我們還有一種簡(jiǎn)便的方式來(lái)實(shí)現(xiàn)ConstraintAttributes支持多選的方式:
struct ConstraintAttributes: OptionSet {
var rawValue: Int
init(rawValue: Int) {
self.rawValue = rawValue
}
static var none = ConstraintAttributes(rawValue: 1 << 0)
static var left = ConstraintAttributes(rawValue: 1 << 1)
static var right = ConstraintAttributes(rawValue: 1 << 2)
}
我們?cè)趯?shí)現(xiàn)過(guò)程中沒(méi)有使用字面量協(xié)議ExpressibleByIntegerLiteral,直接用的位于運(yùn)算香嗓,和SnapKit作者實(shí)現(xiàn)的效果相同爵政。
回到剛才的問(wèn)題,如果在ConstraintItem構(gòu)造方法(target: AnyObject?, attributes: ConstraintAttributes)中attributes傳入.none,它表示對(duì)layoutAttributes數(shù)組不添加NSLayoutConstraint.Attribute元素陶缺。
因?yàn)镾napKit是對(duì)NSLayoutConstraint的封裝钾挟,我們有必要說(shuō)一說(shuō)NSLayoutConstraint這個(gè)類了,它的構(gòu)造方法如下:
/*
item: 指定需要添加約束的視圖一
attribute: 指定視圖一需要約束的屬性
relatedBy: 指定視圖一和視圖二添加約束的關(guān)系
toItem: 指定視圖一依賴關(guān)系的視圖二饱岸;可為nil
attribute: 指定視圖一所依賴的視圖二的屬性掺出,若view2=nil,該屬性設(shè)置 NSLayoutAttributeNotAnAttribute
multiplier: 系數(shù)
情況一:設(shè)置A視圖的高度 = A視圖高度 * multiplier + constant苫费;此時(shí)才會(huì)起作用汤锨;
情況二:設(shè)置A視圖和其他視圖的關(guān)系或 toItem=nil,multiplier設(shè)置不等于0即可百框,若等于0會(huì)crash闲礼;
constant: 常量
layoutConstraint: 返回生成的約束對(duì)象
*/
NSLayoutConstraint(item view1: Any,
attribute attr1: NSLayoutConstraint.Attribute,
relatedBy relation: NSLayoutConstraint.Relation,
toItem view2: Any?,
attribute attr2: NSLayoutConstraint.Attribute,
multiplier: CGFloat,
constant c: CGFloat)
通過(guò)文檔介紹說(shuō)該方法實(shí)際上就是滿足一個(gè)數(shù)學(xué)關(guān)系view1.attr1 = view2.attr2 * multiplier + constant
回到剛才的ConstraintItem類,對(duì)比NSLayoutConstraint的構(gòu)造方法铐维,仔細(xì)觀察它就能發(fā)現(xiàn)柬泽,我們加的約束無(wú)非就是這樣的關(guān)系:
owningView.ConstraintItem = view1 + attr1
toView.ConstraintItem = view2 + attr2
通過(guò)創(chuàng)建兩個(gè)ConstraintItem就能完成基本的約束。了解這點(diǎn)就知道ConstraintItem的作用了嫁蛇。
通過(guò)查看源碼發(fā)現(xiàn)在Constraint類的便利構(gòu)造方法中锨并,對(duì)NSLayoutConstraint進(jìn)行了一層封裝。包括equalTo(view.snp.top) 和 equalTo(view)實(shí)現(xiàn)一樣的效果睬棚,都在這里做了邏輯處理第煮。
最后再來(lái)介紹Constraint類
先來(lái)看這句代碼
let constraint: Constraint = make.top.equal(20).constraint
我們經(jīng)常在外面這樣使用Constraint這個(gè)類,其中ConstraintDescription的作用是用于生產(chǎn)Constraint類抑党,ConstraintDescription的創(chuàng)建在ConstraintMaker類的makeExtendableWithAttributes方法中包警。
實(shí)現(xiàn)一個(gè)動(dòng)畫(huà)效果
var constraint: Constraint?
view.snp.makeConstraints { (make) in
constraint = make.top.equalToSuperview().offset(10).constraint
}
UIView.animateWithDuration(0.3, {
constraint?.update(inset: 20)
self.container.layoutIfNeeded
})
通常我們查看一個(gè)視圖下面是否包含約束時(shí),一般會(huì)直接調(diào)用view.constraints.isEmpty 來(lái)判斷底靠,嚴(yán)謹(jǐn)來(lái)講某個(gè)視圖下可能會(huì)包含一些約束害晦,但這些約束是"不活躍的", 對(duì)視圖顯示不造成任何影響,所以判斷條件需要改一下:
extension UIView {
var isConstraintEmpty: Bool {
return self.constraints.filter { $0.isActive }.isEmpty
}
}