本文为 WWDC 2016 Session 419 的部分内容笔记。强烈推荐观看。
设计师来需求了
在我们的 App 中,通常需要自定义一些视图。例如下图:
我们可能会在很多地方用到右边为内容,左边有个装饰视图的样式,为了代码的通用性,我们在 UITableViewCell
的基础上,封装了一层 DecoratingLayout
,然后再让子类继承它,从而实现这一类视图。
1 2 3 4 5 6 |
class DecoratingLayout : UITableViewCell { var content: UIView var decoration: UIView // Perform layout... } |
重构
但是代码这样组织的话,因为继承自 UITableViewCell
,所以对于其他类型的 view 就不能使用了。我们开始重构。
我们需要让视图布局的功能独立与具体的 view 类型,无论是 UITableViewCell
、UIView
、还是 SKNode
(Sprite Kit 中的类型)
1 2 3 4 5 6 7 8 |
struct DecoratingLayout { var content: UIView var decoration: UIView mutating func layout(in rect: CGRect) { // Perform layout... } } |
这里,我们使用结构体 DecoratingLayout
来表示这种 layout。相比于之前的方式,现在只要在具体的实现中,创建一个 DecoratingLayout
就可以实现布局的功能。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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 的代码来完成布局功能。
通过这种方式实现的布局,对于测试来说也更加的方便:
1 2 3 4 5 6 7 8 9 |
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
上使用上面的布局方式。看如下的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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)来使用。
1 2 3 |
protocol Layout { var frame: CGRect { get set } } |
于是上面两个重复的代码片段又可以合并为:
1 2 3 4 5 6 7 8 9 |
struct DecoratingLayout { var content: Layout var decoration: Layout mutating func layout(in rect: CGRect) { content.frame = ... decoration.frame = ... } } |
为了能够在使用 DecoratingLayout
的时候传入 UIView
和 SKNode
,我们需要让它们遵守 Layout
协议,只需要像下面这样声明一下就可以了,因为二者都已满足协议的要求。
1 2 3 |
extension UIView: Layout {} extension SKNode: Layout {}<code> |
这里讲一点我自己的理解,DreamCell 和 DreamDetailView 中能够使用同一套布局代码,是因为传递进去的 view 都拥有公共的父类 UIView,它提供了 frame 信息,而 UIView 和 SKNode 则不行,这里我们使用 protocol 作为类型参数,可以很好的解决这一问题。
引入范型
然而,目前的代码中是存在一个问题的,content
和 decoration
的具体类型信息在实际中可能是不一致的,因为这里我们只要求了它们的类型信息中提供 frame
属性,而并没有规定它们是相同的类型,例如 content
可能是 UIView
而 decoration
是 SKNode
类型,这与我们的期望是不符的。
这里我们可以通过引入范型来解决:
1 2 3 4 5 6 7 8 9 |
struct DecoratingLayout { var content: Child var decoration: Child mutating func layout(in rect: CGRect) { content.frame = ... decoration.frame = ... } } |
通过使用范型,我们就保证了 content
和 decoration
类型相同。
需求又来啦
设计师说,来,小伙子,完成下面的布局。
为了实现上图的效果,我们仿照之前的写法,实现如下代码:
1 2 3 4 5 6 |
struct CascadingLayout { var children: [Child] mutating func layout(in rect: CGRect) { ... } } |
1 2 3 4 5 6 7 8 9 |
struct DecoratingLayout { var content: Child var decoration: Child mutating func layout(in rect: CGRect) { content.frame = ... |