WWDC-UIKit 中協(xié)議與值類(lèi)型編程實(shí)戰(zhàn)

本文為 WWDC 2016 Session 419 的部分內(nèi)容筆記们何。強(qiáng)烈推薦觀看。

設(shè)計(jì)師來(lái)需求了

在我們的 App 中,通常需要自定義一些視圖彬呻。例如下圖:

Paste_Image.png

我們可能會(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)堪唐。

Paste_Image.png

我們需要讓視圖布局的功能獨(dú)立與具體的 view 類(lèi)型巡语,無(wú)論是 UITableViewCellUIView淮菠、還是 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)
    } 
}

注意觀察上面的代碼,在 UITableViewCellUIView 類(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 = ...
    }
}

注意觀察上面的代碼峦嗤,除了 contentdecoration 的類(lèi)型不一樣之外,其他的都是重復(fù)的代碼屋摔,重復(fù)就是罪惡烁设!

那么我們?nèi)绾尾拍芟@些重復(fù)代碼呢?在 DecoratingLayout 中钓试,唯一用到 contentdecoration 的地方装黑,是獲取它的 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í)候傳入 UIViewSKNode疚颊,我們需要讓它們遵守 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)題的,contentdecoration 的具體類(lèi)型信息在實(shí)際中可能是不一致的翩迈,因?yàn)檫@里我們只要求了它們的類(lèi)型信息中提供 frame 屬性持灰,而并沒(méi)有規(guī)定它們是相同的類(lèi)型,例如 content 可能是 UIViewdecorationSKNode 類(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ò)使用范型喂链,我們就保證了 contentdecoration 類(lèi)型相同。

需求又來(lái)啦

設(shè)計(jì)師說(shuō)妥泉,來(lái)椭微,小伙子,完成下面的布局盲链。

Paste_Image.png

為了實(shí)現(xiàn)上圖的效果蝇率,我們仿照之前的寫(xiě)法,實(shí)現(xiàn)如下代碼:

struct CascadingLayout<Child: Layout> {
    var children: [Child]
    mutating func layout(in rect: CGRect) {
        ...
    }
}
Paste_Image.png
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),就可以得到下面的效果:

Paste_Image.png

組合優(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 { ... }

這里省略了使用 UIViewSKNode 的 frame 來(lái)進(jìn)行布局的代碼何鸡。
于是我們的代碼變成了:

struct DecoratingLayout<Child : Layout> : Layout { ... }
struct CascadingLayout<Child : Layout> : Layout { ... }

看到這里可能有點(diǎn)暈纺弊,其實(shí)代碼表達(dá)的意思是,DecoratingLayout 遵循 Layout 協(xié)議骡男,而它的 contentdecoration 兩個(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

Paste_Image.png

注意觀察上面的視圖龙亲,視圖是有層次結(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 }
}

這里的 contentdecoration 使用的是同樣的 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ì)同步更新。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末爷辙,一起剝皮案震驚了整個(gè)濱河市彬坏,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌膝晾,老刑警劉巖栓始,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異玷犹,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)歹颓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)坯屿,“玉大人,你說(shuō)我怎么就攤上這事巍扛×祯耍” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵撤奸,是天一觀的道長(zhǎng)吠昭。 經(jīng)常有香客問(wèn)我,道長(zhǎng)胧瓜,這世上最難降的妖魔是什么矢棚? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮府喳,結(jié)果婚禮上蒲肋,老公的妹妹穿的比我還像新娘。我一直安慰自己钝满,他們只是感情好兜粘,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著弯蚜,像睡著了一般孔轴。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上碎捺,一...
    開(kāi)封第一講書(shū)人閱讀 51,679評(píng)論 1 305
  • 那天路鹰,我揣著相機(jī)與錄音,去河邊找鬼牵寺。 笑死悍引,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的帽氓。 我是一名探鬼主播趣斤,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼黎休!你這毒婦竟也來(lái)了浓领?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤势腮,失蹤者是張志新(化名)和其女友劉穎联贩,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體捎拯,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡泪幌,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片祸泪。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡吗浩,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出没隘,到底是詐尸還是另有隱情懂扼,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布右蒲,位于F島的核電站阀湿,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏瑰妄。R本人自食惡果不足惜陷嘴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一剖张、第九天 我趴在偏房一處隱蔽的房頂上張望肚菠。 院中可真熱鬧,春花似錦拯坟、人聲如沸眶诈。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)逝撬。三九已至浴骂,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間宪潮,已是汗流浹背溯警。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留狡相,地道東北人梯轻。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像尽棕,于是被迫代替她去往敵國(guó)和親喳挑。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 目錄 0滔悉、前言 一伊诵、Auto Layout前世今生 二、Auto Layout基礎(chǔ)知識(shí) 1.Auto Layout...
    浮游lb閱讀 24,539評(píng)論 3 89
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,166評(píng)論 25 707
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫(kù)回官、插件曹宴、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,105評(píng)論 4 62
  • 一行我 一行抒 一行情 沒(méi)有一行是你 你已遍及我心 出自:西貝
    C帥cc閱讀 223評(píng)論 1 3
  • 春夏秋冬 別送我 這條路我已經(jīng)走過(guò)好多次了。上次回家歉提,這里還只是枯老的枝干笛坦,沒(méi)到春天以前区转,我總以為它已經(jīng)死掉。這一...
    思茲念茲閱讀 318評(píng)論 1 5