本文主要讲述Swift 5.4的新特性 Result Builder在设计上的一些使用方式
需求
假如我们有一个需求,需要往ScrollView上加入不同类型的View,并且根据不确定的的顺序从上往下进行排列,所以一般情况下我们可以这样子设计框架
- 首先定义一个协议,遵循协议的对象使用一个build方法返回一个View
1 | protocol ViewBuilder { |
- 这里我们设计四种颜色的View,宽度均为屏幕宽度,高度不定
1 | struct WhiteView : ViewBuilder { |
最后我们再定义一个ScrollView的容器,传入一个数组,让其从上到下进行排列
1
2
3
4
5
6
7
8
9
10
11
12
13struct ScrollableContainer : ViewBuilder {
var contents : [ViewBuilder]
func build() -> UIView {
let scrollView = UIScrollView.init(frame: UIScreen.main.bounds)
_ = contents.reduce(CGFloat(0)) { currentY, builder in
let view = builder.build()
view.frame.origin.y = currentY
scrollView.addSubview(view)
scrollView.contentSize = CGSize(width: UIScreen.main.bounds.size.width, height: scrollView.subviews.last!.frame.maxY)
return currentY + view.frame.size.height
}
return scrollView
}这样子我们就可以开始布局了
1
2
3
4
5
6let scrollView = ScrollableContainer(contents: [
RedView(),
BlueView(),
GreenView()
])
view.addSubview(scrollView.build())效果如下
通过这种方式,我们就可以随意调整内部的布局顺序,也可以方便的新增多个View
优化
但是上述方法有一个缺点,就是当如果要重复添加多个相同的View或者说想要通过某个条件再添加View,就会有点复杂,比如当needBlue == true
成立的时候再添加BlueView
,那么可能需要这么写
1 | var contents = [ |
为了让可读性更加好,我们可以模仿SwiftUI的DSL语法进行设计,这里就需要使用到Swift 5.4的新特性 Result Builder
首先我们要添加一个容器结构体,因为从上到下写的
View
会被整成一个数组或者多参数传进来,所以要加一个容器把他们从上到下排列好,最后排列完了,再把这个容器放进去ScrollView中1
2
3
4
5
6
7
8
9
10
11
12
13
14struct ViewContainer : ViewBuilder {
var contents : [ViewBuilder]
func build() -> UIView {
let container = UIView(frame: UIScreen.main.bounds)
_ = contents.reduce(CGFloat(0), { currentY, builder in
let view = builder.build()
view.frame.origin.y = currentY
container.addSubview(view)
container.frame.size = CGSize(width: UIScreen.main.bounds.size.width, height: container.subviews.last!.frame.maxY)
return currentY + view.frame.size.height
})
return container
}
}创建一个Result builder,使用
@resultBuilder
注解会要求我们添加一个buildBlock
方法,实现这个方法,把我们外面传进来的多个View放进去VieContainer
容器中,然后实现buildFinalResult
在编写结束的时候把VieContainer
放进去ScrollView
容器中1
2
3
4
5
6
7
8
9
struct ScrollableViewBuilder {
static func buildBlock(_ components: ViewBuilder...) -> ViewBuilder {
return ViewContainer(contents: components)
}
static func buildFinalResult(_ component: ViewBuilder) -> ViewBuilder {
return ScrollableContainer(contents: [component])
}
}这时候我们通过新增的
ScrollableViewBuilder
来创建一个方法,这里的闭包就是待会我们要写DSL的地方1
2
3func build( content: () -> ViewBuilder) -> ViewBuilder {
return content()
}这个方法传入一个闭包,这个由于我们已经实现了
buildBlock
,所以content
被@ScrollableViewBuilder
修饰之后,会自动将闭包内的东西转化成多参数,传入buildBlock
方法,在那里面我们把各种各样的View
给添加到ViewContainer
上,方法调用如下1
2
3
4
5
6
7let result = build {
RedView()
BlueView()
GreenView()
}
view.addSubview(result.build())这时候运行效果同上图一致
接下来我们需要让这个闭包内支持if语句、else if语句、else语句和for语句,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17struct ScrollableViewBuilder {
static func buildBlock(_ components: ViewBuilder...) -> ViewBuilder {
return ScrollableContainer(contents: components)
}
/// 表示if语句
static func buildEither(first component: ViewBuilder) -> ViewBuilder {
return component
}
/// 表示eles if语句
static func buildEither(second component: ViewBuilder) -> ViewBuilder {
return component
}
/// 表示else语句和其他的可选值(即?修饰的View)
static func buildOptional(_ component: ViewBuilder?) -> ViewBuilder {
return component ?? DefaultView()
}
}然后我们试一试这么写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20let flag = 2
let result = build {
RedView()
BlueView()
GreenView()
if flag == 1 {
RedView()
RedView()
} else if flag == 2 {
GreenView()
GreenView()
GreenView()
} else if flag == 3 {
GreenView()
} else {
BlueView()
BlueView()
BlueView()
}
}当flag = 1的时候,首先两个
RedView()
会先进入buildBlock
方法,然后被包装成一个ViewContainer
,然后再进去buildEither(first component: ViewBuilder)
方法,这里我没处理就直接把传进来的值返回出去了当flag = 2 或者 flag = 3 的时候,首先括号内的多个
GreenView()
会先进入buildBlock
方法,然后被包装成一个ViewContainer
,然后再进去buildEither(second component: ViewBuilder)
方法,这里我没处理就直接把传进来的值返回出去了当 flag 为其他值的时候,首先括号内的多个
BlueView()
会先进入buildBlock
方法,然后被包装成一个ViewContainer
,然后再进去buildEither(second component: ViewBuilder)
方法。当没有写eles
语句的时候,需要实现buildOptional(_ component: ViewBuilder?)
方法,去处理没有进入if
语句而导致的不返回View
的情况,如果没有写else
语句,那么不会进入buildBlock
,会直接取空值情况下你返回的默认View
。处理for语句,比如这样
1
2
3
4
5
6
7
8
9let result = build {
RedView()
BlueView()
GreenView()
for _ in 0...2 {
GreenView()
BlueView()
}
}for
里面需要返回3个GreenView+BlueView
,这里他每次调用for的括号里面的内容,都会走一遍buildBlock
把里面的GreenView+BlueView
封装成一个ViewContainer
,所以这里会产生3个ViewContainer
,最后这三个会变成一个数组,进入buildArray
方法,再封装成一个ViewContainer
,代码如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct ScrollableViewBuilder {
static func buildBlock(_ components: ViewBuilder...) -> ViewBuilder {
return ViewContainer(contents: components)
}
static func buildEither(first component: ViewBuilder) -> ViewBuilder {
return component
}
static func buildEither(second component: ViewBuilder) -> ViewBuilder {
return component
}
static func buildOptional(_ component: ViewBuilder?) -> ViewBuilder {
return component ?? WhiteView()
}
static func buildArray(_ components: [ViewBuilder]) -> ViewBuilder {
return ViewContainer(contents: components)
}
static func buildFinalResult(_ component: ViewBuilder) -> ViewBuilder {
return ScrollableContainer(contents: [component])
}
}处理表达式,如果我们要在DSL里面插一些除了
View
之外的一些东西,那么就需要添加对应的处理方法,比如1
2
3
4
5
6let result = build {
print("a123")
RedView()
BlueView()
GreenView()
}针对这个
print
我们添加表达式处理ScrollableViewBuilder
1
2
3
4
5
6
7
8
9/// 针对正常的表达式,就直接返回
static func buildExpression(_ expression: ViewBuilder) -> ViewBuilder {
return expression
}
/// 针对特殊的表达式,返回空View
static func buildExpression(_ expression: ()) -> ViewBuilder {
return EmptyBuilder()
}EmptyBuilder
1
2
3
4
5struct EmptyBuilder : ViewBuilder {
func build() -> UIView {
return UIView.init()
}
}
总结
到这里就说的差不多了,其他本文没提及到的内容可以参阅Write a DSL in Swift using result builders
本文Demo地址