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

设计师来需求了

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

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

class DecoratingLayout : UITableViewCell {

var content: UIView

var decoration: UIView

// Perform layout...

}

重构

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

我们需要让视图布局的功能独立与具体的 view 类型,无论是 UITableViewCell、UIView、还是 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)

}

}

注意观察上面的代码,在 UITableViewCell 和 UIView 类型的 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 = ...

}

}

注意观察上面的代码,除了 content 和 decoration 的类型不一样之外,其他的都是重复的代码,重复就是罪恶!

那么我们如何才能消除这些重复代码呢?在 DecoratingLayout 中,唯一用到 content 和 decoration 的地方,是获取它的 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 的时候传入 UIView 和 SKNode,我们需要让它们遵守 Layout 协议,只需要像下面这样声明一下就可以了,因为二者都已满足协议的要求。

extension UIView: Layout {}

extension SKNode: Layout {}<code>

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

引入范型

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

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

struct DecoratingLayout {

var content: Child

var decoration: Child

mutating func layout(in rect: CGRect) {

content.frame = ...

decoration.frame = ...

}

}

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

需求又来啦

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

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

struct CascadingLayout {

var children: [Child]

mutating func layout(in rect: CGRect) {

...

}

}

struct DecoratingLayout {

var content: Child

var decoration: Child

mutating func layout(in rect: CGRect) {

content.frame = ...

decoration.frame = ...

}

}

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

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

组合优于继承

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

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

protocol Layout {

mutating func layout(in rect: CGRect)

}

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

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

extension UIView: Layout { ... }

extension SKNode: Layout { ... }

这里省略了使用 UIView 和 SKNode 的 frame 来进行布局的代码。

于是我们的代码变成了:

struct DecoratingLayout : Layout { ... }

struct CascadingLayout : Layout { ... }

看到这里可能有点晕,其实代码表达的意思是,DecoratingLayout 遵循 Layout 协议,而它的 content 和 decoration 两个 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

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

我们来修改 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 : Layout {

...

mutating func layout(in rect: CGRect)

typealias Content = Child.Content

var contents: [Content] { get }

}

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

范型牛逼!*3

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

struct DecoratingLayout : Layout {

var content: Child

var decoration: Child

mutating func layout(in rect: CGRect)

typealias Content = Child.Content

var contents: [Content] { get }

}

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

struct DecoratingLayout : 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!

WWDC-UIKit 中协议与值类型编程实战的更多相关文章

  1. Asp.net MVC 中Controller返回值类型ActionResult

    [Asp.net MVC中Controller返回值类型] 在mvc中所有的controller类都必须使用"Controller"后缀来命名并且对Action也有一定的要求: 必 ...

  2. Controller 中Action 返回值类型 及其 页面跳转的用法

        •Controller 中Action 返回值类型 View – 返回  ViewResult,相当于返回一个View 页面. -------------------------------- ...

  3. Web API中的返回值类型

    WebApi中的返回值类型大致可分为四种: Void/ IHttpActionResult/ HttpResponseMessage /自定义类型 一.Void void申明方法没有返回值,执行成功后 ...

  4. C#中,为什么在值类型后面加问号

    在C#中,声明一个值类型或引用类型的变量,无论是否给这个变量赋初值,该变量都有默认值: 比如声明引用类型变量: string a,其等效于string a = null,string的默认值为null ...

  5. 关于C#编程中引用与值类型赋值的一些容易犯错的地方

    值类型与引用类型的区别在于:值类型在赋值的时候是拷贝值,引用类型在赋值的时候的拷贝引用.记住这一个原则,我们再来分析一些具体情况: PointStruct pt1 = ,); PointStruct ...

  6. C#中引用类型和值类型

    C#的值类型包括:结构体(数值类型,bool型,用户定义的结构体),枚举,可空类型. C#的引用类型包括:数组,用户定义的类.接口.委托,object,字符串. 值类型和引用类型的区别在于,值类型的变 ...

  7. C#中 哪些是值类型 哪些是引用类型

    DateTime属于 结构类型,所以是  值类型 在 C#中 简单类型,结构类型,枚举类型是值类型:其余的:接口,类,字符串,数组,委托都是引用类型

  8. C#中引用类型和值类型的区别,分别有哪些

    C#的值类型包括:结构体(数值类型,bool型,用户定义的结构体),枚举,可空类型. C#的引用类型包括:数组,用户定义的类.接口.委托,object,字符串. 数组的元素,不管是引用类型还是值类型, ...

  9. MVC 中Controller返回值类型ActionResult

    下面列举Asp.net MVC中Controller中的ActionResult返回类型 1.返回ViewResult视图结果,将视图呈现给网页 public ActionResult About() ...

随机推荐

  1. python判断一个数字是整数还是浮点数

    在网上闲逛,发现了一个python的用法

  2. 关于Keil的安装与注册

    由于前一段时间一直在做关于stm32f407的相关内容,于是安装的Keil是MDK5,最近一阵子想再看看51单片机以前没有做过的内容,就要再安装一个Keil C51,结果就不可避免的遇到了两个软件必须 ...

  3. codevs 1066 引水入城

    传送门 题目描述 Description 在一个遥远的国度,一侧是风景秀美的湖泊,另一侧则是漫无边际的沙漠.该国的行政 区划十分特殊,刚好构成一个N行M列的矩形,如上图所示,其中每个格子都代表一座城 ...

  4. codevs 1557 热浪

    传送门 题目描述 Description 德克萨斯纯朴的民眾们这个夏天正在遭受巨大的热浪!!!他们的德克萨斯长角牛吃起来不错,可是他们并不是很擅长生產富含奶油的乳製品.Farmer John此时以先天 ...

  5. Label设置行间距--b

    内容摘要 UILabel显示多行文本 UILabel设置行间距 解决单行文本 & 多行文本显示的问题 场景描述 众所周知,UILabel显示多行的话,默认行间距为0,但实际开发中,如果显示多行 ...

  6. (转) 基于Theano的深度学习(Deep Learning)框架Keras学习随笔-01-FAQ

    特别棒的一篇文章,仍不住转一下,留着以后需要时阅读 基于Theano的深度学习(Deep Learning)框架Keras学习随笔-01-FAQ

  7. Fibonacci Tree

    hdu4786:http://acm.hdu.edu.cn/showproblem.php?pid=4786 题意:给你一个无向图,然后其中有的边是白色的有的边是黑色的.然后问你是否存在一棵生成树,在 ...

  8. Alternating Current

    Codeforces Round #200 (Div. 1) B:http://codeforces.com/problemset/problem/343/B 题意:这一题看懂题意是关键,题目的意思就 ...

  9. 关于使用Element.getNodeValue()返回NULL的问题

    使用DOM方法解析XML数据, 比如这个xml:<root>  <name>admin</name></root> 在程序中读取 Document do ...

  10. 修改表增加字段默认值default

    对个生产库的表增加1个字段.字段类型是INT型, 表数据有2千万条, alter table table_name add xxoo number(4) default  0 ; 因此 不仅要修改字典 ...