當(dāng)頁(yè)面復(fù)雜起來(lái)的時(shí)候,或者說(shuō),當(dāng)頁(yè)面控件數(shù)量太多的時(shí)候,自動(dòng)布局的代碼也會(huì)逐漸繁瑣起來(lái),造成了 View Controller 中的代碼繁重起來(lái)
因此,這里提供一種方法,將 UI 的布局代碼移動(dòng)到其他文件中,這個(gè)方法受到了 ?? 這篇文章 的啟發(fā),大部分的代碼也是從這里出來(lái)
布局的方法使用自動(dòng)布局,而且還是用 SnapKit 的那種,語(yǔ)言使用的是 Swift 3
創(chuàng)建一個(gè)關(guān)于自動(dòng)布局的協(xié)議
先創(chuàng)建一個(gè)文件,普通的 Swift 文件的那種,并在里面 import SnapKit
定義一個(gè) protocol
protocol Layoutable {
func layoutMaker() -> (ConstraintMaker) -> Void
func layoutUpdater() -> (ConstraintMaker) -> Void
}
其中 layoutMaker() 使用來(lái)創(chuàng)建約束,layoutUpdater() 是用來(lái)更新約束
這里的 (ConstraintMaker) -> Void 是什么?
看下面一段代碼,這是我們布局自定義控件的一般寫(xiě)法,至少是我吧
ivCalendar.snp.makeConstraints { (make) in
make.left.equalTo(contentView).offset(PreviewCellConstants.marginToFrame)
make.centerY.equalTo(lbDate)
make.size.equalTo(PreviewCellConstants.LabelCalendar.size)
}
再看 makeConstraints 方法的定義
public func makeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
ConstraintMaker.makeConstraints(item: self.view, closure: closure)
}
可以看出,我們寫(xiě)的一大串的布局代碼,其實(shí)是一個(gè) Closure,類型是 (ConstraintMaker) -> Void,而這里,就是我們用來(lái)分拆代碼的關(guān)鍵了
對(duì)于上面的 func layoutUpdater() -> (ConstraintMaker) -> Void 協(xié)議方法,很多時(shí)候,我們只需要 make,而不太需要 update,這里可以選擇將它刪除。但是,也可以為它實(shí)現(xiàn)一個(gè)默認(rèn)的實(shí)現(xiàn),也就是 protocol 的 extension
如果沒(méi)用到
func layoutUpdater() -> (ConstraintMaker) -> Void這個(gè)方法,而且不作處理(刪除它,或者提供默認(rèn)實(shí)現(xiàn)),那么,在 implement 協(xié)議的時(shí)候,編譯器將就會(huì)報(bào)錯(cuò):沒(méi)有 conforms to protocol
Layoutabel 的默認(rèn)實(shí)現(xiàn),只是簡(jiǎn)單地返回一個(gè)空的 Closure,當(dāng)我們有實(shí)現(xiàn)對(duì)應(yīng)的方法時(shí),調(diào)用的將會(huì)是后者
extension Layoutable {
func layoutMaker() -> (ConstraintMaker) -> Void {
return { make in
}
}
func layoutUpdater() -> (ConstraintMaker) -> Void {
return { make in
}
}
}
為 UIView 添加布局相關(guān)的方法
在與協(xié)議的同一個(gè)文件中,順便向 UIView 添加一個(gè) extension,添加布局方法,這個(gè)方法,是之后在 View Controller 中用到的一行代碼實(shí)現(xiàn)布局的方法
// MARK: - 自動(dòng)布局的 extension
extension UIView {
func makeLayout(layouter: Layoutable) {
snp.makeConstraints(layouter.layoutMaker())
}
func updateLayout(layouter: Layoutable) {
snp.updateConstraints(layouter.layoutUpdater())
}
}
可以看到,snp.makeConstraints(...) 這一句,正是我們布局時(shí)寫(xiě)得算是最多的一句代碼了。但現(xiàn)在,我們將會(huì)很少看到它了
同時(shí),我們也可以看到,snp.makeConstraints(...) 中傳入的參數(shù),都是通過(guò)協(xié)議來(lái)調(diào)用的(大致意思,意會(huì)一下吧),這也許就是 面向協(xié)議編程 的抽象吧
之后,我們的工作將會(huì)聚焦在:如何創(chuàng)建一個(gè)實(shí)現(xiàn) Layoutable 協(xié)議的類和它的實(shí)例
為 UI 控件的布局代碼創(chuàng)建一個(gè)文件
下面的工作,可能就顯得略煩瑣,甚至看起來(lái)有點(diǎn)“蠢”了
為 UI 控件創(chuàng)建一個(gè)文件
為你覺(jué)得需要的 UI 控件,創(chuàng)建一個(gè)普通的 Swift 文件,并 import SnapKit
創(chuàng)建一個(gè) struct,并聲明實(shí)現(xiàn) Layoutable 協(xié)議
struct ClipRecordPanelLayout: Layoutable {
}
為什么使用 struct,而不是使用 class ?
我的理解是這樣的:
對(duì)于 class,在賦值的時(shí)候,實(shí)例會(huì)對(duì)其屬性(對(duì)象的那種)默認(rèn)進(jìn)行了一個(gè)強(qiáng)引用,而這種強(qiáng)引用,很多時(shí)候就是造成內(nèi)存問(wèn)題(像循環(huán)引用)的原因
而對(duì)于 struct,在賦值的時(shí)候,實(shí)例只會(huì)對(duì)屬性進(jìn)行一系列的復(fù)制,不帶引用的復(fù)制,因此可以避免出現(xiàn)內(nèi)存問(wèn)題
定義一些屬性
struct ClipRecordPanelLayout: Layoutable {
var views: (UIView)
var constants: (panelHeight: CGFloat?, bottomOffset: CGFloat?)
}
在布局的時(shí)候,我們通常需要其他的一些 view 來(lái)作為參照物,并且需要寫(xiě)死一些常量,因此,在這里,使用了 views 和 constants 來(lái)分別裝載這些參照物和常量
views 和 constants 都是 tuple 的類型,使用 tuple 的原因是,tuple 可以(天生)存放不同類型的數(shù)據(jù),并且,可以為其中的值進(jìn)行命名
在這個(gè)例子中,constants 存放的都是 CGFloat? 類型,但也可以存放不同的類型,如
var constants: (bottomOffset: CGFloat, size: CGSize)
實(shí)現(xiàn)協(xié)議方法
struct ClipRecordPanelLayout: Layoutable {
var views: (UIView)
var constants: (panelHeight: CGFloat?, bottomOffset: CGFloat?)
func layoutMaker() -> (ConstraintMaker) -> Void {
let superView = views
let (panelHeight, _) = constants
return { make in
make.bottom.equalTo(superView)
make.left.right.equalTo(superView)
make.height.equalTo(panelHeight!)
}
}
func layoutUpdater() -> (ConstraintMaker) -> Void {
let superView = views
let (_, bottomOffset) = constants
return { make in
make.bottom.equalTo(superView).offset(bottomOffset!)
}
}
}
這里分別實(shí)現(xiàn)了 layoutMaker() 和 layoutUpdater() 的方法,分別對(duì)應(yīng)的是建立約束和更新約束的操作
在獲取參照物和常量的時(shí)候,使用了 tuple 的解構(gòu),將數(shù)據(jù)分別存放到不同的變量中,不需要的數(shù)據(jù),直接使用 _ 進(jìn)行忽略
在 constants 中,其中的元素的數(shù)據(jù)類型使用的 CGFloat?,是因?yàn)檫@兩個(gè)數(shù)據(jù),并不是同時(shí)都需要用到,那么,為了方便起見(jiàn),在需要某一個(gè)參數(shù)的時(shí)候,直接傳 nil
所以,這里的一個(gè)文件,只定義了一類的約束(也許有多個(gè) UI 控件的布局方式是一樣的,就是常量不同罷了)
View Controller 中一句話完成控件的布局
定義完了一類控件的約束之后,最后,在 View Controller 中將約束應(yīng)用到 UI 控件中,像這樣:
panel.makeLayout(layouter: ClipRecordPanelLayout(with: (view), constants: (PanelHeight, nil)))
這樣,就一句話實(shí)現(xiàn)了對(duì) UI 控件的布局了,當(dāng)然這是針對(duì)簡(jiǎn)單的情況,要是含有多個(gè)參照物和常量,還是需要將 Layoutable 的實(shí)例單獨(dú)抽取出來(lái)新建,避免一行代碼過(guò)長(zhǎng)
References
對(duì)了,還有這個(gè)。。。
到我托放在 GitHub 的地方閱讀能進(jìn)行開(kāi)始跳轉(zhuǎn) ??