本文为 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. Multilingual App Toolkit v2.2 release

    Multilingual App Toolkit v2.2 release Today we released Multilingual App Toolkit v2.2. This release ...

  2. HDFS 搭建记录

    1. 三台服务: 172.17.0.62(namenode) 172.17.0.68(datanode) 172.17.0.76(datanode) /etc/hosts包含的内容: 三台都包含的域名 ...

  3. JSP特点

    建立在servlet规范功能之上的动态网页技术. JSP文件在用户第一次请求时,会被编译成servlet,然后由servlet处理用户的请求.所以JSP可以看成运行时servlet. 1).将内容的生 ...

  4. Java实现Http服务器(二)

    上节讲到的JDK自带的HttpServer组件,实现方法大概有三十个类构成,下面尝试着理解下实现思路. 由于Java的source代码中有很多注释,粘贴上来看着费劲,自己写个程序消除注释. impor ...

  5. Android Audio 分析

    一.架构 二.MediaServer初始化 所有的media服务都在进程mediaserver里.其代码在framework/base/media/mediaserver/main_mediaserv ...

  6. PYTHON文本处理指南之日志LOG解析

    处理特定字段的内容,并指指定条件输出. 注意代码中用一个方法列表,并且将方法参数延后传递. GOOGLE作过PYTHON代码的水平,就是不一样呀. 希望能学到这种通用的技巧. 只是,英文PDF看起来有 ...

  7. AlarmManager.setRepeating将不再准确

    背景: 当我们想让Android应用程序定时为做一件工作时,我们往往会在一个BroadcastReceiver中使用AlarmManager.setRepeating()方法来实现.在API 19(即 ...

  8. BZOJ1600: [Usaco2008 Oct]建造栅栏

    1600: [Usaco2008 Oct]建造栅栏 Time Limit: 5 Sec  Memory Limit: 64 MBSubmit: 825  Solved: 473[Submit][Sta ...

  9. 增加几个entity framework 的函数 (记录备忘)[转]

    public static class DatabaseExtensions { public static DataTable SqlQueryForDataTatable(this Databas ...

  10. (经常看看)jdk 设计模式

    在JDK(Java Development Kit)类库中,开发人员使用了大量设计模式,正因为如此,我们可以在不修改JDK源码的前提下开发出自己的应用软件,本文列出了部分JDK中的模式应用实例,有兴趣 ...