Arclin

Advocate Technology. Enjoy Technology.

0%

Swift 5.4 Result Builder

本文主要讲述Swift 5.4的新特性 Result Builder在设计上的一些使用方式

需求

假如我们有一个需求,需要往ScrollView上加入不同类型的View,并且根据不确定的的顺序从上往下进行排列,所以一般情况下我们可以这样子设计框架

  1. 首先定义一个协议,遵循协议的对象使用一个build方法返回一个View
1
2
3
protocol ViewBuilder {
func build() -> UIView
}
  1. 这里我们设计四种颜色的View,宽度均为屏幕宽度,高度不定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
struct WhiteView : ViewBuilder {
func build() -> UIView {
let view = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 100))
view.backgroundColor = .white
return view
}
}

struct RedView : ViewBuilder {
func build() -> UIView {
let banner = UIView.init(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 200))
banner.backgroundColor = UIColor.red
return banner
}
}

struct BlueView : ViewBuilder {
func build() -> UIView {
let goodsView = UIView.init(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 350))
goodsView.backgroundColor = UIColor.blue
return goodsView
}
}

struct GreenView : ViewBuilder {
func build() -> UIView {
let dynamicView = UIView.init(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 400))
dynamicView.backgroundColor = UIColor.green
return dynamicView
}
}
  1. 最后我们再定义一个ScrollView的容器,传入一个数组,让其从上到下进行排列

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    struct 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
    }
  2. 这样子我们就可以开始布局了

    1
    2
    3
    4
    5
    6
    let scrollView = ScrollableContainer(contents: [
    RedView(),
    BlueView(),
    GreenView()
    ])
    view.addSubview(scrollView.build())

    效果如下

  3. 通过这种方式,我们就可以随意调整内部的布局顺序,也可以方便的新增多个View

优化

但是上述方法有一个缺点,就是当如果要重复添加多个相同的View或者说想要通过某个条件再添加View,就会有点复杂,比如当needBlue == true成立的时候再添加BlueView,那么可能需要这么写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var contents = [
RedView(),
GreenView()
]

if needBlue == true {
contents = [
RedView(),
BlueView(),
GreenView()
]
}

let scrollView = ScrollableContainer(contents: contents)
view.addSubview(scrollView.build())

为了让可读性更加好,我们可以模仿SwiftUI的DSL语法进行设计,这里就需要使用到Swift 5.4的新特性 Result Builder

  1. 首先我们要添加一个容器结构体,因为从上到下写的View会被整成一个数组或者多参数传进来,所以要加一个容器把他们从上到下排列好,最后排列完了,再把这个容器放进去ScrollView中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    struct 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
    }
    }
  2. 创建一个Result builder,使用@resultBuilder注解会要求我们添加一个buildBlock方法,实现这个方法,把我们外面传进来的多个View放进去VieContainer容器中,然后实现buildFinalResult在编写结束的时候把
    VieContainer放进去ScrollView容器中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @resultBuilder
    struct ScrollableViewBuilder {
    static func buildBlock(_ components: ViewBuilder...) -> ViewBuilder {
    return ViewContainer(contents: components)
    }
    static func buildFinalResult(_ component: ViewBuilder) -> ViewBuilder {
    return ScrollableContainer(contents: [component])
    }
    }
  3. 这时候我们通过新增的ScrollableViewBuilder来创建一个方法,这里的闭包就是待会我们要写DSL的地方

    1
    2
    3
    func build(@ScrollableViewBuilder content: () -> ViewBuilder) -> ViewBuilder {
    return content()
    }

    这个方法传入一个闭包,这个由于我们已经实现了buildBlock,所以content@ScrollableViewBuilder修饰之后,会自动将闭包内的东西转化成多参数,传入buildBlock方法,在那里面我们把各种各样的View给添加到ViewContainer上,方法调用如下

    1
    2
    3
    4
    5
    6
    7
    let result = build {
    RedView()
    BlueView()
    GreenView()
    }

    view.addSubview(result.build())

    这时候运行效果同上图一致

  4. 接下来我们需要让这个闭包内支持if语句、else if语句、else语句和for语句,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    struct 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
    20
    let 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

  5. 处理for语句,比如这样

    1
    2
    3
    4
    5
    6
    7
    8
    9
    let 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
    @resultBuilder
    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])
    }
    }
  6. 处理表达式,如果我们要在DSL里面插一些除了View之外的一些东西,那么就需要添加对应的处理方法,比如

    1
    2
    3
    4
    5
    6
    let 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
    5
    struct EmptyBuilder : ViewBuilder {
    func build() -> UIView {
    return UIView.init()
    }
    }

总结

到这里就说的差不多了,其他本文没提及到的内容可以参阅Write a DSL in Swift using result builders

本文Demo地址