WWDC-UIKit 中协议与值类型编程实战

本文为 WWDC 2016 Session 419 的部分内容笔记。强烈推荐观看。

设计师来需求了

在我们的 App 中,通常需要自定义一些视图。例如下图:

Paste_Image.png

我们可能会在很多地方用到右边为内容,左边有个装饰视图的样式,为了代码的通用性,我们在 UITableViewCell 的基础上,封装了一层 DecoratingLayout,然后再让子类继承它,从而实现这一类视图。

class DecoratingLayout : UITableViewCell {
    var content: UIView
    var decoration: UIView
    
    // Perform layout...
}

重构

但是代码这样组织的话,因为继承自 UITableViewCell,所以对于其他类型的 view 就不能使用了。我们开始重构。

Paste_Image.png

我们需要让视图布局的功能独立与具体的 view 类型,无论是 UITableViewCellUIView、还是 SKNode(Sprite Kit 中的类型)

struct DecoratingLayout {
    var content: UIView
    var decoration: UIView
    
    mutating func layout(in rect: CGRect) {
        // Perform layout...
    }
}

这里,我们使用结构体 DecoratingLayout 来表示这种 layout。相比于之前的方式,现在只要在具体的实现中,创建一个 DecoratingLayout 就可以实现布局的功能。代码如下:

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 类型的 view 中,布局功能和具体的视图已经解耦,我们都可以使用 struct 的代码来完成布局功能。

通过这种方式实现的布局,对于测试来说也更加的方便:

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))
}

我们的野心远不止于此。这里我们也想要在 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 的类型不一样之外,其他的都是重复的代码,重复就是罪恶!

那么我们如何才能消除这些重复代码呢?在 DecoratingLayout 中,唯一用到 contentdecoration 的地方,是获取它的 frame 属性,所以,如果这两个 property 的类型信息中,能够提供 frame 就可以了,于是我们想到了使用 protocol 作为类型(type)来使用。

protocol Layout {
    var frame: CGRect { get set }
}

于是上面两个重复的代码片段又可以合并为:

struct DecoratingLayout {
    var content: Layout
    var decoration: Layout
    
    mutating func layout(in rect: CGRect) {
        content.frame = ...
        decoration.frame = ...
    }
}

为了能够在使用 DecoratingLayout 的时候传入 UIViewSKNode,我们需要让它们遵守 Layout 协议,只需要像下面这样声明一下就可以了,因为二者都已满足协议的要求。

extension UIView: Layout {}
extension SKNode: Layout {}

这里讲一点我自己的理解,DreamCell 和 DreamDetailView 中能够使用同一套布局代码,是因为传递进去的 view 都拥有公共的父类 UIView,它提供了 frame 信息,而 UIView 和 SKNode 则不行,这里我们使用 protocol 作为类型参数,可以很好的解决这一问题。

引入范型

然而,目前的代码中是存在一个问题的,contentdecoration 的具体类型信息在实际中可能是不一致的,因为这里我们只要求了它们的类型信息中提供 frame 属性,而并没有规定它们是相同的类型,例如 content 可能是 UIViewdecorationSKNode 类型,这与我们的期望是不符的。

这里我们可以通过引入范型来解决:

struct DecoratingLayout<Child: Layout> {
    var content: Child
    var decoration: Child
    
    mutating func layout(in rect: CGRect) {
        content.frame = ...
        decoration.frame = ...
    }
}

通过使用范型,我们就保证了 contentdecoration 类型相同。

需求又来啦

设计师说,来,小伙子,完成下面的布局。

Paste_Image.png

为了实现上图的效果,我们仿照之前的写法,实现如下代码:

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 = ...
    }
}

这里我又将前面的代码拿了过来,方便查看。

我们将上面的两种布局方式组合起来,就可以得到下面的效果:

Paste_Image.png

组合优于继承

那么如何才能将两种布局方式组合起来呢?

来观察我们之前定义的协议 Layout,其实我们关心的并不是 Layout 中的 frame,我们的目的是,让 Layout 能够在特定的上下文中进行相应的布局,所以我们来修改代码:

protocol Layout {
    mutating func layout(in rect: CGRect)
}

这里 Layout 的语义变成了:该类型能够在特定的 CGRect 中进行相应的布局。

同时我们也需要修改代码:

extension UIView: Layout { ... }
extension SKNode: Layout { ... }

这里省略了使用 UIViewSKNode 的 frame 来进行布局的代码。
于是我们的代码变成了:

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

看到这里可能有点晕,其实代码表达的意思是,DecoratingLayout 遵循 Layout 协议,而它的 contentdecoration 两个 property 也同样遵循该协议,即可以在特定的 CGRect 中完成布局操作。而两个结构体本身就包含 layout 操作,所以不需要任何其他的代码,结构体做的事情就是,在自己进行 layout 操作的基础上,将其传递给两个 property 然后分别进行 layout,这就完成了组合

组合之后的执行代码如下:

let decoration = CascadingLayout(children: accessories) // 左边
var composedLayout = DecoratingLayout(content: content, decoration: decoration) // 整体
composedLayout.layout(in: rect) // 执行 layout 操作

On step further

Paste_Image.png

注意观察上面的视图,视图是有层次结构的,所以我们需要在布局的时候,能够拿到这个子视图数组,之前的视实现方式中,只能布局单个的视图,没有办法拿到整个视图数组进行操作。
我们来修改 Layout 的代码:

protocol Layout {
    mutating func layout(in rect: CGRect)
    var contents: [Layout] { get }
}

这里增加了一个可读属性,返回一个 Layout 数组。同样,这里的代码存在一个问题,contents 可以为不同的 Layout 类型,例如 [UIView(), SKNode()],所以为了让 contents 中的类型一致,我们使用 associatedtype,将上面的代码改写为:

protocol Layout {
    mutating func layout(in rect: CGRect)
    associatedtype Content
    var contents: [Content] { get }
}

相应的 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 }
}

重复就是罪恶啊!可以看到,这里唯一的不同只是 Content 的类型信息。这里我们还是利用强大的范型来解决:

struct DecoratingLayout<Child : Layout> : Layout {
   ...
   mutating func layout(in rect: CGRect)
   typealias Content = Child.Content
   var contents: [Content] { get }
}

这里,当 Child 范型确定的时候,Child.Content 的类型信息也相应地确定了,所以可以使用上面的代码来消除重复。

范型牛逼!*3

别激动的太早,我们的代码中还存在一个问题。目前我们的代码长这样:

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 方式,这与我们的预期是不符的。我们的需求时视图左边和右边使用不同的布局方式。然而我们又需要这个范型的方式来保证它们俩实际的数据类型是相同的,这里需要使用两个范型信息,但是限制它们的实际数据类型相同。修改后的代码如下:

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 }
}

以上。

再一次,推荐你在写 Swift 中定义新类型的时候,把 class 抛在脑后,尝试着从 struct 和 protocol 开始。

Happy Hacking!

如果你希望使用 rss 的方式,可以订阅我的博客,文章将会同步更新。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249

推荐阅读更多精彩内容

  • 目录 0、前言 一、Auto Layout前世今生 二、Auto Layout基础知识 1.Auto Layout...
    浮游lb阅读 23,594评论 3 89
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,472评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,551评论 4 58
  • 一行我 一行抒 一行情 没有一行是你 你已遍及我心 出自:西贝
    C帥cc阅读 202评论 1 3
  • 春夏秋冬 别送我 这条路我已经走过好多次了。上次回家,这里还只是枯老的枝干,没到春天以前,我总以为它已经死掉。这一...
    思兹念兹阅读 262评论 1 5