WWDC-UIKit 中协议与值类型编程实战
本文为 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 中协议与值类型编程实战的更多相关文章
- Asp.net MVC 中Controller返回值类型ActionResult
[Asp.net MVC中Controller返回值类型] 在mvc中所有的controller类都必须使用"Controller"后缀来命名并且对Action也有一定的要求: 必 ...
- Controller 中Action 返回值类型 及其 页面跳转的用法
•Controller 中Action 返回值类型 View – 返回 ViewResult,相当于返回一个View 页面. -------------------------------- ...
- Web API中的返回值类型
WebApi中的返回值类型大致可分为四种: Void/ IHttpActionResult/ HttpResponseMessage /自定义类型 一.Void void申明方法没有返回值,执行成功后 ...
- C#中,为什么在值类型后面加问号
在C#中,声明一个值类型或引用类型的变量,无论是否给这个变量赋初值,该变量都有默认值: 比如声明引用类型变量: string a,其等效于string a = null,string的默认值为null ...
- 关于C#编程中引用与值类型赋值的一些容易犯错的地方
值类型与引用类型的区别在于:值类型在赋值的时候是拷贝值,引用类型在赋值的时候的拷贝引用.记住这一个原则,我们再来分析一些具体情况: PointStruct pt1 = ,); PointStruct ...
- C#中引用类型和值类型
C#的值类型包括:结构体(数值类型,bool型,用户定义的结构体),枚举,可空类型. C#的引用类型包括:数组,用户定义的类.接口.委托,object,字符串. 值类型和引用类型的区别在于,值类型的变 ...
- C#中 哪些是值类型 哪些是引用类型
DateTime属于 结构类型,所以是 值类型 在 C#中 简单类型,结构类型,枚举类型是值类型:其余的:接口,类,字符串,数组,委托都是引用类型
- C#中引用类型和值类型的区别,分别有哪些
C#的值类型包括:结构体(数值类型,bool型,用户定义的结构体),枚举,可空类型. C#的引用类型包括:数组,用户定义的类.接口.委托,object,字符串. 数组的元素,不管是引用类型还是值类型, ...
- MVC 中Controller返回值类型ActionResult
下面列举Asp.net MVC中Controller中的ActionResult返回类型 1.返回ViewResult视图结果,将视图呈现给网页 public ActionResult About() ...
随机推荐
- RegularExpressionValidator 常用
RegularExpressionValidator 控件用于验证输入值是否匹配正则表达式指定的模式 属性: ControlToValidate="要验证的控件名称" Valida ...
- Sublime text 3 快键方式汇总 及 主题应用
Sublime Text 3 快捷键汇总 Sublime Text 3是款非常实用代码编辑神器,但是想要用任何一款软件,掌握一些快捷键还是很有必要的. 选择类 Ctrl+D 选中光标所占的文本,继续操 ...
- 大话设计模式之策略模式(strategy)
策略模式:它定义了算法家族,分别封装起来,让他们之间可以互相替换,此模式让算法的变化不会影响使用算法的用户. 针对商城收银模式,打折,返现促销等的例子: 打折还是促销其实都是一些算法,可以用工厂模式来 ...
- Uva_11361 Investigating Div-Sum Property
题目链接 题意: 在[A,B]区间内找出满足条件的数有多少个. 条件:这个数本身 能够整除K, 且各位数字之和能够整除K. 思路: 数据范围过大2^31 2^31 = 2147483648 ~ 2*1 ...
- liveReload
依赖条件: 1.安装liveReload浏览器插件: http://livereload.com/extensions/ chrome可以直接去在线商店安装liveReload. P.S.也可以贴代码 ...
- python获取本地ip地址的方法
#_*_coding:utf8_*_ #以下两种方法可以在ubuntu下或者windows下获得本地的IP地址 import socket # 方法一 localIP = socket.gethost ...
- [BZOJ 3218] A + B Problem 【可持久化线段树 + 网络流】
题目连接:BZOJ - 3218 题目分析 题目要求将 n 个点染成黑色或白色,那么我们可以转化为一个最小割模型. 我们规定一个点 i 最后属于 S 集表示染成黑色,属于 T 集表示染成白色,那么对于 ...
- Long Long Message
poj2774:http://poj.org/problem?id=2774 题意:求两个串的最长公共子串. 题解:求出后缀数组,然后求height数组,找出最大的值,并且这两个子串在不同的原串中即可 ...
- struts2 集成webservice 的方法
由于项目需求的需要,要在原来用Struts2的框架之上集成webservice,因为之前单单做webservice的时候没有多大问题,使用 Spring 和 Xfire就可以轻松地发布服务,但是,当和 ...
- SQL in查询报告类型转换失败的3种解决办法
-- in查询 nvarchar转int 错误 (NodeId 为 int 类型) ) = '3,5,6,' )' SELECT ID , NodeName FROM WF_WorkFlowNode ...