簡介
什么是Snapkit
- SnapKit是一個(gè)使用 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)特定領(lǐng)域語言
DSL是為了解決某些特定場景下的任務(wù)而專門設(shè)計(jì)的語言。如果能把一些設(shè)計(jì)師產(chǎn)出的長寬油够、色值蚁袭、文字、居中石咬、距上等設(shè)計(jì)元數(shù)據(jù)(設(shè)計(jì)的標(biāo)注信息等)揩悄,以一種約定的簡潔的語言規(guī)則(即DSL)輸入給程序代碼,由程序和代碼自動(dòng)的分析和處理鬼悠,從而生成真正的界面開發(fā)代碼setFrame删性,setTitle,setColor厦章,addSubview镇匀,這樣就可以大幅度的減少代碼量與工作量,程序員來寫這種簡潔的語法規(guī)則會(huì)更快更高效袜啃,甚至可以把這種簡潔的語法規(guī)則教會(huì)設(shè)計(jì)師汗侵,讓設(shè)計(jì)師有能力直接寫出DSL,然后輸入給底層程序,這樣界面就自然完成晰韵。
注意事項(xiàng)
- 使用SnapKit前发乔,一定要先將子控件添加到父視圖中,否則會(huì)直接崩潰雪猪!
parentView.addSubview(subview)
- leading和left栏尚、trailing和right
其實(shí)在目前國內(nèi)App中使用leading與left,trailing與right在正常情況下是等價(jià)的只恨,這是因?yàn)閲鴥?nèi)的閱讀習(xí)慣是從左到右的译仗,不過如果你的App需要在阿拉伯國家上架,他們的布局是從右至左時(shí)(比如阿拉伯文) 則會(huì)對(duì)調(diào)官觅。
建議使用leading和trailing纵菌,便于App國際化。
使用教程
- 較為簡單休涤,api也不多咱圆,不多描述了
源碼解析
詳細(xì)解析
- snp
lable.snp通過給view加擴(kuò)展實(shí)現(xiàn)的
public extension ConstraintView {
public var snp: ConstraintViewDSL {
return ConstraintViewDSL(view: self
}
}
snp 最后是生成了一個(gè) ConstraintViewDSL 對(duì)象
- ConstraintView的定義
if os(iOS) || os(tvOS)
public typealias ConstraintView = UIView
#else public
typealias ConstraintView = NSView
#endif
這里tvOS是基于 iOS的操作系統(tǒng),tvOS 是專門為第四代 Apple TV設(shè)計(jì)的操作系統(tǒng)功氨。
- ConstraintViewDSL
internal init(view: ConstraintView) {
self.view = view
}
ConstraintViewDSL 類的構(gòu)造函數(shù)序苏,就是將 view 保存起來
public func makeConstraints(_ closure:
(_ make: ConstraintMaker) -> Void){
ConstraintMaker.makeConstraints(item:self.view, closure: closure)
}
makeConstraints 函數(shù)將傳進(jìn)來的閉包傳遞給ConstraintMaker 這個(gè)類去處理了
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)
}
}
該方法主要調(diào)用了被接受prepareConstraints函數(shù)。
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)造一個(gè) maker捷凄,然后調(diào)用閉包忱详,閉包內(nèi)部會(huì)添加一些約束,接下來就是獲取這些約束, 最后將約束激活纵势。
閉包就是能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)踱阿。例如在程序中,只有函數(shù)內(nèi)部的子函數(shù)才能讀取局部變量钦铁,所以閉包可以理解成“定義在一個(gè)函數(shù)內(nèi)部的函數(shù)“在本質(zhì)上软舌,閉包是將函數(shù)內(nèi)部和函數(shù)外部連接起來的橋梁。
internal init(item: LayoutConstraintItem) {
self.item = item
self.item.prepare()
}
這是ConstraintMaker的構(gòu)造函數(shù)牛曹,這里出現(xiàn)了一個(gè)新的類型LayoutConstraintItem佛点,表示一個(gè)可布局的對(duì)象。
public protocol LayoutConstraintItem: class {
}
可以看到這是一個(gè)協(xié)議
extension ConstraintLayoutGuide : LayoutConstraintItem {
}
extension ConstraintView : LayoutConstraintItem {
}
ConstraintView 和 ConstraintLayoutGuide 都實(shí)現(xiàn)LayoutConstraintItem這個(gè)協(xié)議黎比。
extension LayoutConstraintItem {
internal func prepare() {
if let view = self as? ConstraintView {
view.translatesAutoresizingMaskIntoConstraints = false
}
}
}
該協(xié)議實(shí)現(xiàn)了一些方法,包含prepare方法超营。這一步其實(shí)就是禁用 View 的 AutoresizeMask。
回到開始的閉包阅虫,里面我們寫的make.center.equalTo(self.view.snp.center)可以通過這個(gè)函數(shù)生成一些約束對(duì)象演闭。首先我們都知道, 每一個(gè)約束, 首先需要添加到一個(gè)對(duì)象上面, 還需要約束的屬性颓帝,關(guān)系大于米碰、等于窝革、小于,如果不是常量類型吕座,還需要另一個(gè)依賴的對(duì)象虐译,以及依賴的屬性,系數(shù)以及一個(gè)偏移常量吴趴。
這里的 make.center就是說添加到當(dāng)前漆诽,并設(shè)置約束屬性center,equalTo锣枝,則是表示關(guān)系為等于厢拭,self.view.snp.center則表示依賴的對(duì)象是 self.view,依賴的屬性也是 center惊橱,系數(shù)及偏移值這里均沒有指定蚪腐,表示使用默認(rèn)值。
public var center: ConstraintMakerExtendable {
return self.makeExtendableWithAttributes(.center)
}
這個(gè)只是一個(gè)簡便方法税朴, 具體的實(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
internal struct ConstraintAttributes : OptionSet, ExpressibleByIntegerLiteral {
}
ConstraintAttributes 本身是一個(gè) 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
}
初始化颤殴,成為統(tǒng)一可操作的類型觅廓。
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 本身是一個(gè) OptionSet,里面定義了許多屬性涵但, 例如 left, right, center使用 OptionSet 的意義在于杈绸,可以通過組合操作,同時(shí)添加多個(gè)屬性矮瘟,例如瞳脓,center這個(gè)屬性就是由 centerX 和 centerY 復(fù)合而來。
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
}
這個(gè)類是一個(gè)描述類澈侠,用于描述一條具體的約束劫侧,里面包含了約束的屬性,關(guān)系等回到ConstraintMaker.makeConstraints 中的第一個(gè) for 循環(huán)哨啃,里面就是去獲取description.constraint 已達(dá)到最終構(gòu)造約束的目的烧栋。
public class ConstraintMakerExtendable: ConstraintMakerRelatable {
public var left: ConstraintMakerExtendable {
self.description.attributes += .left
return self
}
...
}
makeExtendableWithAttributes最后返回的時(shí)候, 返回的是一ConstraintMakerExtendable對(duì)象。這個(gè)類的主要目的是為了實(shí)現(xiàn)鏈?zhǔn)降亩鄬傩匀颍纾?code>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,這個(gè)類主要是負(fù)責(zé)構(gòu)造一個(gè)關(guān)系祝峻,例如 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 只是對(duì)內(nèi)部函數(shù)relatedTo 的一個(gè)簡單調(diào)用
public protocol ConstraintRelatableTarget {
}
extension Int: ConstraintRelatableTarget {
}
extension UInt: ConstraintRelatableTarget {
}
extension Float: ConstraintRelatableTarget {
}
extension ConstraintItem: ConstraintRelatableTarget {
}
extension ConstraintView: ConstraintRelatableTarget {
}
ConstraintRelatableTarget是一個(gè)協(xié)議魔吐,表示一個(gè)可以被依賴的目標(biāo)次坡,我們在手寫 NSLayoutConstraint 的時(shí)候,
依賴對(duì)象可以為 view画畅,可以為ConstraintLayoutGuide砸琅,也可以為空,為空的時(shí)候轴踱,表示使用絕對(duì)值症脂,該協(xié)議分別有 Int、 Double淫僻、CGPoint等字面值诱篷,也有UIView, ConstraintLayoutGuide雳灵,同時(shí)棕所,也有ConstraintItem,讓我們可以指定依賴的具體值, 我們之前的代碼 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) } …
其他均類似针贬。可以看到這里面構(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
}
}
- ConstraintMakerEditable
ConstraintMakerEditable 這個(gè)類主要是設(shè)置Autolayout 中的兩個(gè)常量multiplier 和 constant 與優(yōu)先級(jí)拢蛋,使用方法如make.center.equalTo(self.view.snp.center).offset(20)
再次回到makeConstraints桦他,通過上面的若干步驟,完成了對(duì) 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這個(gè)類主要就是生成和操縱 NSLayoutConstraint。構(gòu)造函數(shù)有點(diǎ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
……
函數(shù)中第一行的self.layoutConstraints = []使用來存放所有最后生成的NSLayoutConstraint
后面的兩行是獲取兩個(gè)對(duì)象的約束屬性蔫劣。而 layoutFrom則是約束屬性的起始對(duì)象,在我們最初那段代碼中皆警,就表示了snplabel這個(gè)視圖拦宣。
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)
}
}
后面則是獲取約束的關(guān)系, 如等于, 大于。主要的代碼都在那個(gè)循環(huán)中信姓,主要邏輯是遍歷添加在起始對(duì)象上的約束屬性鸵隧,然后獲取預(yù)支對(duì)應(yīng)的目標(biāo)對(duì)象及目標(biāo)對(duì)象的約束屬性,最后生成LayoutConstraint
其中第一個(gè) if else 分支中在確定目標(biāo)屬性該使用何種值, 通過分析可以看出, 我們之前那段代碼, 其實(shí)可以將make.center.equalTo(self.view.snp.center)
中直接寫為make.center.equalTo(self.view)
后面則是根據(jù)不同的目標(biāo)屬性意推,獲取適當(dāng)?shù)钠浦刀固薄R约矮@取目標(biāo)對(duì)象。
后面 LayoutConstraint(xxx) 中的 LayoutConstraint 其實(shí)只是一個(gè)NSLayoutConstraint 的子類菊值,只是在其中添加了一個(gè)標(biāo)簽與創(chuàng)建者(Constraint) 的引用
- activateIfNeeded
makeConstraints最后一步則是激活, 在 iOS 8 以前外驱, 所有的依賴屬性育灸, 都必須使用 view.addConstraint(xxx)方法將依賴激活, iOS 8 后昵宇, 則直接將依賴激活即可生效磅崭。activateIfNeeded 則是將依賴激活使其生效