本文為 WWDC 2016 Session 419 的部分內(nèi)容筆記们何。強(qiáng)烈推薦觀看。
設(shè)計(jì)師來(lái)需求了
在我們的 App 中,通常需要自定義一些視圖彬呻。例如下圖:
我們可能會(huì)在很多地方用到右邊為內(nèi)容,左邊有個(gè)裝飾視圖的樣式柄瑰,為了代碼的通用性闸氮,我們?cè)?UITableViewCell
的基礎(chǔ)上,封裝了一層 DecoratingLayout
教沾,然后再讓子類(lèi)繼承它蒲跨,從而實(shí)現(xiàn)這一類(lèi)視圖。
class DecoratingLayout : UITableViewCell {
var content: UIView
var decoration: UIView
// Perform layout...
}
重構(gòu)
但是代碼這樣組織的話授翻,因?yàn)槔^承自 UITableViewCell
或悲,所以對(duì)于其他類(lèi)型的 view 就不能使用了。我們開(kāi)始重構(gòu)堪唐。
我們需要讓視圖布局的功能獨(dú)立與具體的 view 類(lèi)型巡语,無(wú)論是 UITableViewCell
、UIView
淮菠、還是 SKNode
(Sprite Kit 中的類(lèi)型)
struct DecoratingLayout {
var content: UIView
var decoration: UIView
mutating func layout(in rect: CGRect) {
// Perform layout...
}
}
這里男公,我們使用結(jié)構(gòu)體 DecoratingLayout
來(lái)表示這種 layout。相比于之前的方式合陵,現(xiàn)在只要在具體的實(shí)現(xiàn)中理澎,創(chuàng)建一個(gè) DecoratingLayout
就可以實(shí)現(xiàn)布局的功能逞力。代碼如下:
class DreamCell : UITableViewCell {
...
override func layoutSubviews() {
var decoratingLayout = DecoratingLayout(content: content, decoration: decoration)
decoratingLayout.layout(in: bounds)
}
}
class DreamDetailView : UIView {
...
override func layoutSubviews() {
var decoratingLayout = DecoratingLayout(content: content, decoration: decoration)
decoratingLayout.layout(in: bounds)
}
}
注意觀察上面的代碼,在 UITableViewCell
和 UIView
類(lèi)型的 view 中糠爬,布局功能和具體的視圖已經(jīng)解耦寇荧,我們都可以使用 struct 的代碼來(lái)完成布局功能。
通過(guò)這種方式實(shí)現(xiàn)的布局执隧,對(duì)于測(cè)試來(lái)說(shuō)也更加的方便:
func testLayout() {
let child1 = UIView()
let child2 = UIView()
var layout = DecoratingLayout(content: child1, decoration: child2)
layout.layout(in: CGRect(x: 0, y: 0, width: 120, height: 40))
XCTAssertEqual(child1.frame, CGRect(x: 0, y: 5, width: 35, height: 30))
XCTAssertEqual(child2.frame, CGRect(x: 35, y: 5, width: 70, height: 30))
}
我們的野心遠(yuǎn)不止于此揩抡。這里我們也想要在 SKNode
上使用上面的布局方式《屏穑看如下的代碼:
struct ViewDecoratingLayout {
var content: UIView
var decoration: UIView
mutating func layout(in rect: CGRect) {
content.frame = ...
decoration.frame = ...
}
}
struct NodeDecoratingLayout {
var content: SKNode
var decoration: SKNode
mutating func layout(in rect: CGRect) {
content.frame = ...
decoration.frame = ...
}
}
注意觀察上面的代碼峦嗤,除了 content
和 decoration
的類(lèi)型不一樣之外,其他的都是重復(fù)的代碼屋摔,重復(fù)就是罪惡烁设!
那么我們?nèi)绾尾拍芟@些重復(fù)代碼呢?在 DecoratingLayout
中钓试,唯一用到 content
和 decoration
的地方装黑,是獲取它的 frame
屬性,所以弓熏,如果這兩個(gè) property 的類(lèi)型信息中恋谭,能夠提供 frame 就可以了,于是我們想到了使用 protocol 作為類(lèi)型(type)來(lái)使用挽鞠。
protocol Layout {
var frame: CGRect { get set }
}
于是上面兩個(gè)重復(fù)的代碼片段又可以合并為:
struct DecoratingLayout {
var content: Layout
var decoration: Layout
mutating func layout(in rect: CGRect) {
content.frame = ...
decoration.frame = ...
}
}
為了能夠在使用 DecoratingLayout
的時(shí)候傳入 UIView
和 SKNode
疚颊,我們需要讓它們遵守 Layout
協(xié)議,只需要像下面這樣聲明一下就可以了信认,因?yàn)槎叨家褲M足協(xié)議的要求材义。
extension UIView: Layout {}
extension SKNode: Layout {}
這里講一點(diǎn)我自己的理解,DreamCell 和 DreamDetailView 中能夠使用同一套布局代碼嫁赏,是因?yàn)閭鬟f進(jìn)去的 view 都擁有公共的父類(lèi) UIView母截,它提供了 frame 信息,而 UIView 和 SKNode 則不行橄教,這里我們使用 protocol 作為類(lèi)型參數(shù),可以很好的解決這一問(wèn)題喘漏。
引入范型
然而护蝶,目前的代碼中是存在一個(gè)問(wèn)題的,content
和 decoration
的具體類(lèi)型信息在實(shí)際中可能是不一致的翩迈,因?yàn)檫@里我們只要求了它們的類(lèi)型信息中提供 frame
屬性持灰,而并沒(méi)有規(guī)定它們是相同的類(lèi)型,例如 content
可能是 UIView
而 decoration
是 SKNode
類(lèi)型负饲,這與我們的期望是不符的堤魁。
這里我們可以通過(guò)引入范型來(lái)解決:
struct DecoratingLayout<Child: Layout> {
var content: Child
var decoration: Child
mutating func layout(in rect: CGRect) {
content.frame = ...
decoration.frame = ...
}
}
通過(guò)使用范型喂链,我們就保證了 content
和 decoration
類(lèi)型相同。
需求又來(lái)啦
設(shè)計(jì)師說(shuō)妥泉,來(lái)椭微,小伙子,完成下面的布局盲链。
為了實(shí)現(xiàn)上圖的效果蝇率,我們仿照之前的寫(xiě)法,實(shí)現(xiàn)如下代碼:
struct CascadingLayout<Child: Layout> {
var children: [Child]
mutating func layout(in rect: CGRect) {
...
}
}
struct DecoratingLayout<Child: Layout> {
var content: Child
var decoration: Child
mutating func layout(in rect: CGRect) {
content.frame = ...
decoration.frame = ...
}
}
這里我又將前面的代碼拿了過(guò)來(lái)刽沾,方便查看本慕。
我們將上面的兩種布局方式組合起來(lái),就可以得到下面的效果:
組合優(yōu)于繼承
那么如何才能將兩種布局方式組合起來(lái)呢侧漓?
來(lái)觀察我們之前定義的協(xié)議 Layout
锅尘,其實(shí)我們關(guān)心的并不是 Layout
中的 frame
,我們的目的是布蔗,讓 Layout
能夠在特定的上下文中進(jìn)行相應(yīng)的布局藤违,所以我們來(lái)修改代碼:
protocol Layout {
mutating func layout(in rect: CGRect)
}
這里 Layout
的語(yǔ)義變成了:該類(lèi)型能夠在特定的 CGRect
中進(jìn)行相應(yīng)的布局。
同時(shí)我們也需要修改代碼:
extension UIView: Layout { ... }
extension SKNode: Layout { ... }
這里省略了使用 UIView
和 SKNode
的 frame 來(lái)進(jìn)行布局的代碼何鸡。
于是我們的代碼變成了:
struct DecoratingLayout<Child : Layout> : Layout { ... }
struct CascadingLayout<Child : Layout> : Layout { ... }
看到這里可能有點(diǎn)暈纺弊,其實(shí)代碼表達(dá)的意思是,DecoratingLayout
遵循 Layout
協(xié)議骡男,而它的 content
和 decoration
兩個(gè) property 也同樣遵循該協(xié)議淆游,即可以在特定的 CGRect
中完成布局操作。而兩個(gè)結(jié)構(gòu)體本身就包含 layout
操作隔盛,所以不需要任何其他的代碼犹菱,結(jié)構(gòu)體做的事情就是,在自己進(jìn)行 layout
操作的基礎(chǔ)上吮炕,將其傳遞給兩個(gè) property 然后分別進(jìn)行 layout
腊脱,這就完成了組合。
組合之后的執(zhí)行代碼如下:
let decoration = CascadingLayout(children: accessories) // 左邊
var composedLayout = DecoratingLayout(content: content, decoration: decoration) // 整體
composedLayout.layout(in: rect) // 執(zhí)行 layout 操作
On step further
注意觀察上面的視圖龙亲,視圖是有層次結(jié)構(gòu)的陕凹,所以我們需要在布局的時(shí)候,能夠拿到這個(gè)子視圖數(shù)組鳄炉,之前的視實(shí)現(xiàn)方式中杜耙,只能布局單個(gè)的視圖,沒(méi)有辦法拿到整個(gè)視圖數(shù)組進(jìn)行操作拂盯。
我們來(lái)修改 Layout
的代碼:
protocol Layout {
mutating func layout(in rect: CGRect)
var contents: [Layout] { get }
}
這里增加了一個(gè)可讀屬性佑女,返回一個(gè) Layout
數(shù)組。同樣,這里的代碼存在一個(gè)問(wèn)題团驱,contents 可以為不同的 Layout 類(lèi)型摸吠,例如 [UIView(), SKNode()]
,所以為了讓 contents
中的類(lèi)型一致嚎花,我們使用 associatedtype
寸痢,將上面的代碼改寫(xiě)為:
protocol Layout {
mutating func layout(in rect: CGRect)
associatedtype Content
var contents: [Content] { get }
}
相應(yīng)的 struct 改為:
struct ViewDecoratingLayout : Layout {
...
mutating func layout(in rect: CGRect)
typealias Content = UIView
var contents: [Content] { get }
}
struct NodeDecoratingLayout : Layout {
...
mutating func layout(in rect: CGRect)
typealias Content = SKNode
var contents: [Content] { get }
}
重復(fù)就是罪惡啊贩幻!可以看到轿腺,這里唯一的不同只是 Content
的類(lèi)型信息。這里我們還是利用強(qiáng)大的范型來(lái)解決:
struct DecoratingLayout<Child : Layout> : Layout {
...
mutating func layout(in rect: CGRect)
typealias Content = Child.Content
var contents: [Content] { get }
}
這里丛楚,當(dāng) Child
范型確定的時(shí)候族壳,Child.Content
的類(lèi)型信息也相應(yīng)地確定了,所以可以使用上面的代碼來(lái)消除重復(fù)趣些。
范型牛逼仿荆!*3
別激動(dòng)的太早,我們的代碼中還存在一個(gè)問(wèn)題坏平。目前我們的代碼長(zhǎng)這樣:
struct DecoratingLayout<Child : Layout> : Layout {
var content: Child
var decoration: Child
mutating func layout(in rect: CGRect)
typealias Content = Child.Content
var contents: [Content] { get }
}
這里的 content
和 decoration
使用的是同樣的 layout 方式拢操,這與我們的預(yù)期是不符的。我們的需求時(shí)視圖左邊和右邊使用不同的布局方式舶替。然而我們又需要這個(gè)范型的方式來(lái)保證它們倆實(shí)際的數(shù)據(jù)類(lèi)型是相同的令境,這里需要使用兩個(gè)范型信息,但是限制它們的實(shí)際數(shù)據(jù)類(lèi)型相同顾瞪。修改后的代碼如下:
struct DecoratingLayout<Child : Layout, Decoration : Layout
where Child.Content == Decoration.Content> : Layout {
var content: Child
var decoration: Decoration
mutating func layout(in rect: CGRect)
typealias Content = Child.Content
var contents: [Content] { get }
}
以上舔庶。
再一次,推薦你在寫(xiě) Swift 中定義新類(lèi)型的時(shí)候陈醒,把 class 拋在腦后惕橙,嘗試著從 struct 和 protocol 開(kāi)始。
Happy Hacking!
如果你希望使用 rss 的方式钉跷,可以訂閱我的博客弥鹦,文章將會(huì)同步更新。