本文为 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. 单元测试unit test,集成测试integration test和功能测试functional test的区别

    以下内容转自 https://codeutopia.net/blog/2015/04/11/what-are-unit-testing-integration-testing-and-function ...

  2. iOS: 填充数据表格

    功能:创建一个列表并填充 // // main.m // Hello // // Created by lishujun on 14-8-28. // Copyright (c) 2014年 lish ...

  3. Ecshop wap

    http://www.08kan.com/gwk/MzA3MDMxMzAxMw/200091492/1/c38b5937e4e819d9908fe3ae964e3dfc.html

  4. 监控Activity在前后台状态的切换

    public class BaseActivity extends Activity{ @Overrideprotected void onStop() { boolean isOnForegroun ...

  5. 【HDOJ】4902 Nice boat

    区间线段树.题目还不错. /* */ #include <iostream> #include <string> #include <map> #include & ...

  6. bzoj1389

    比较有意思的一道题初看肯定是dp一类,但好像没什么思路,先令p=1-p q=1-q如果我们用常见的f[i]到第i次试验最大利润的话我们发现不好转移,因为影响因素不仅有价格,还有数量考虑到原料总量一定, ...

  7. 踩过的坑之-----selector

    打算踏踏实实的做技术了,以前总是毛毛躁躁的将代码粘贴复制完事能跑起来就行.最近慢慢感觉这样真的对自己的时间和经历是一种浪费. 就从最基本的做起吧,今天做了一个selector,在按钮上面添加效果, & ...

  8. unity5 人皮渲染 Skin Shading

    换了一种方法,优化了一下代码,unity5效果很好,消耗不大 点开可查看大图 加入了次表面散射的阴影与自阴影              ------------by wolf96  wolf_crix ...

  9. Unity3d 使用DX11的曲面细分

    Unity3d surface Shaderswith DX11 Tessellation Unity3d surface shader 在DX11上的曲面细分 I write this articl ...

  10. bzoj 1026 [SCOI2009]windy数(数位DP)

    1026: [SCOI2009]windy数 Time Limit: 1 Sec  Memory Limit: 162 MBSubmit: 4550  Solved: 2039[Submit][Sta ...