Result Builders 实战

5/1/2022 swiftiosswiftui

Result Builders允许某些函数通过一系列组件中隐式构建结果值,按照开发者设定的构建规则对组件进行排列。通过对函数语句应用构建器进行转译,Result Builders提供了在 Swift 中创建新的领域特定语言(DSL)的能力。

与常见的使用点语法实现的类 DSL 相比,使用Result Builders创建的 DSL 使用更简单、无效内容更少、代码更容易理解。

目前苹果在SwiftUI框架中大量地使用了该功能,除了最常见的视图构建器(ViewBuilder)外,其他还包括:AccessibilityRotorContentBuilder、CommandsBuilder、LibraryContentBuilder、SceneBuilder、TableColumnBuilder、TableRowBuilder、ToolbarContentBuilder、WidgetBundleBuilder 等。

本文将制作一个 Result Builder,用声明式的方式定义 AttributeString 使得代码更加干净、易读。

# 常规定义 greet

我们来定义个 greet 函数,来实现一个简单的字符串拼接功能:

func greet(name: String, title: String) -> NSMutableAttributedString {
    let attributes = [NSAttributedString.Key.foregroundColor : UIColor.red]
    let attributes2 = [
      NSAttributedString.Key.font : UIFont.systemFont(ofSize: 20),
      NSAttributedString.Key.foregroundColor : UIColor.blue
    ]

    let message = NSMutableAttributedString()
    message.append(NSAttributedString(string: "Hello "))
    message.append(NSAttributedString(string: name, attributes: attributes))
    message.append(NSMutableAttributedString(string: ", "))
    message.append(NSAttributedString(string: title, attributes: attributes2))
    return message
}

greet(name: "读者们", title: "欢迎👏🏻")

运行的结果:

那么如何通过 Result builders 将代码改造成类似下面:

func greet(name: String, title: String) -> NSAttributedString {
  NSAttributedString(string: "Hello ")
  NSAttributedString(string: name, attributes: ...)
  NSAttributedString(string: ",")
  NSAttributedString(string: title, attributes: ...)
}

不需要 return 返回语句,且不需要手动拼接字符串。下面,我们一起来实现它。

# 创建 result builder

一个result builder类型必须满足两个基本要求:

  • 它必须通过@resultBuilder进行标注,这表明它打算作为一个结果构建器类型使用,并允许它作为一个自定义属性使用。
  • 它必须至少实现一个名为buildBlock的类型方法。

那么:

@resultBuilder
enum AttributedStringBuilder {
    static func buildBlock(_ components: NSAttributedString...) -> NSAttributedString {
        let attributedString = NSMutableAttributedString()
        for component in components {
            attributedString.append(component)
        }
        return attributedString
    }
}

然后我们通过这个result builder来实现这个greet的函数,并且命名这个函数为greetBuilder

@AttributedStringBuilder
func greetBuilder(name: String, title: String) -> NSAttributedString {
    NSMutableAttributedString(string: "Hello ")
    NSMutableAttributedString(string: name, attributes:[
        .foregroundColor : .red
    ])
    NSMutableAttributedString(string: ", ")
    NSMutableAttributedString(string: title, attributes:[
      .font : .systemFont(ofSize: 20),
      .foregroundColor : .blue
    ])
}

greetBuilder(name: "读者们", title: "Are you ok?")

为了让代码看得更加简洁,我们对 NSMutableAttributedString 添加扩展功能。

extension NSMutableAttributedString {
    public func color(_ color: UIColor) -> NSMutableAttributedString {
        self.addAttribute(.foregroundColor, value: color, range: .init(location: 0, length: self.length))
        return self
    }

    public func font(_ font: UIFont) -> NSMutableAttributedString {
        self.addAttribute(.font, value: font, range: .init(location: 0, length: self.length))
        return self
    }
}

我们的greetBuilder代码:

@AttributedStringBuilder
func greetBuilder(name: String, title: String) -> NSAttributedString {
    NSMutableAttributedString(string: "Hello ")
    NSMutableAttributedString(string: name).color(.red)
    NSMutableAttributedString(string: ", ")
    NSMutableAttributedString(string: title)
        .font(.systemFont(ofSize: 20))
        .color(.blue)
}

greetBuilder(name: "读者们", title: "Are you ok?")

不知道你是否觉得 NSMutableAttributedString 太长了,我们进行优化下:

typealias Text = NSMutableAttributedString

最终我们的完整代码如下:

public typealias Text = NSMutableAttributedString

extension Text {
    public func color(_ color: UIColor) -> Text {
        self.addAttribute(.foregroundColor, value: color, range: .init(location: 0, length: self.length))
        return self
    }

    public func font(_ font: UIFont) -> Text {
        self.addAttribute(.font, value: font, range: .init(location: 0, length: self.length))
        return self
    }
}

@resultBuilder
enum AttributedStringBuilder {
    static func buildBlock(_ components: Text...) -> Text {
        let attributedString = Text()
        for component in components {
            attributedString.append(component)
        }
        return attributedString
    }
}

@AttributedStringBuilder
func greetBuilder(name: String, title: String) -> Text {
    Text(string: "Hello ")
    Text(string: name)
        .color(.red)
    Text(string: ", ")
    Text(string: title)
        .font(.systemFont(ofSize: 20))
        .color(.blue)
}

greetBuilder(name: "读者们", title: "Are you ok?")

greetBuilder 是不是看起来有那味了。

# 使用条件语句

如果传递过去的 title 是个空字符串:

greetBuilder(name: "读者们", title: "")
// Hello 读者们,

结尾出现了 ,我们不希望显示出来。需要去判断 title 是否为空,如果为空,则不添加 。所以我们需要支持if语句:

if !title.isEmpty {
  Text(string: ",")
  Text(string:title)
    .font(.systemFont(ofSize: 20))
    .color(.blue)
}

为了让 Result builder 支持条件逻辑,我们需要在AttributedStringBuilder添加新的方法。

static func buildOptional(_ component: Text?) -> Text {
    component ?? Text(string: "")
}

buildOptional 用于处理在特定执行中可能或不可能出现的部分结果。当一个结果构建器提供了 buildOptional(_😃 时,转译后的函数可以使用没有 else 的 if 语句,同时也提供了对 if let 的支持。

# 更复杂的条件逻辑

如果标题为空,我们想显示为 谢谢来访。那么我们就在 if 语句中补全 else 语句

if !title.isEmpty {
  ...
} else {
  Text(string: ",谢谢来访")
}

buildOptional 只适用于 if 且没有 else 语句的情况。通过报错提示可知,AttributedStringBuilder需要实现 buildEither(first:)buildEither(second:) 方法。

static func buildEither(first component: Text) -> Text {
    component
}

static func buildEither(second component: Text) -> Text {
    component
}

buildEither(first: Component) -> ComponentbuildEither(second: Component) -> Component,用于在选择语句的不同条件下建立部分结果。当一个结果构建器提供这两个方法的实现时,转译后的函数可以使用带有elseif语句以及 switch语句。

# 使用循环

如果我问候不是读者们,而是指定的一组人,比如:小华,小明,张三,李四。那么我们将greetBuilder将变为如下:

@AttributedStringBuilder
func greetBuilder(names: [String], title: String) -> Text {
    Text(string: "Hello ")
    for name in names {
        Text(string: name)
            .color(.red)
        Text(string: " ")
    }
    if !title.isEmpty {
        Text(string: ",")
        Text(string:title)
        .font(.systemFont(ofSize: 20))
        .color(.blue)
    } else {
        Text(string: ",谢谢来访")
    }
}

greetBuilder(names: ["小华", "小明", "张三", "李四"], title: "欢迎来访👏🏻")

同理,根据报错提示,如果要实现循环,那么AttributedStringBuilder需要实现buildArr(_:)方法。

static func buildArray(_ components: [Text]) -> Text {
    let attr = Text()
    for com in components {
        attr.append(com)
    }
    return attr
}

这块代码跟 buildBlock 的实现相似。添加后,报错消失,可以看到如下结果:

# 支持多种类型

比如我们可以将空格和逗号做一下封装:


enum SpecialCharacters {
    case space
    case comma
}

然后将空格和逗号替换掉:

@AttributedStringBuilder
func greetBuilder(names: [String], title: String) -> Text {
    Text(string: "Hello ")
    for name in names {
        Text(string: name)
            .color(.red)
        SpecialCharacters.space
    }
    if !title.isEmpty {
        SpecialCharacters.comma
        Text(string:title)
        .font(.systemFont(ofSize: 20))
        .color(.blue)
    } else {
        SpecialCharacters.comma
        Text(string: "谢谢来访")
    }
}

greetBuilder(names: ["小华", "小明", "张三", "李四"], title: "欢迎来访👏🏻")

为了支持自定义类型,我们需要实现 buildExpression(_:):

static func buildExpression(_ expression: SpecialCharacters) -> Text {
    switch expression {
    case .comma:
        return Text(string: ",")
    case .space:
        return Text(string: " ")
    }
}

static func buildExpression(_ expression: Text) -> Text {
    expression
}

添加完成后,报错消失了。

buildExpression(_ expression: Expression) -> Component它允许结果构建器区分表达式类型和组件类型,为语句表达式提供上下文类型信息。构建器会将 SpecialCharacters 首先转换成 Text,然后再将其传入到 buildBlock 中。

# 样例完整代码


public typealias Text = NSMutableAttributedString

extension Text {
    public func color(_ color: UIColor) -> Text {
        self.addAttribute(.foregroundColor, value: color, range: .init(location: 0, length: self.length))
        return self
    }

    public func font(_ font: UIFont) -> Text {
        self.addAttribute(.font, value: font, range: .init(location: 0, length: self.length))
        return self
    }
}

@resultBuilder
enum AttributedStringBuilder {

    static func buildBlock(_ components: Text...) -> Text {
        let attributedString = Text()
        for component in components {
            attributedString.append(component)
        }
        return attributedString
    }

    static func buildOptional(_ component: Text?) -> Text {
      component ?? Text(string: "")
    }

    static func buildEither(first component: Text) -> Text {
      component
    }

    static func buildEither(second component: Text) -> Text {
      component
    }

    static func buildArray(_ components: [Text]) -> Text {
        let attr = Text()
        for com in components {
            attr.append(com)
        }
        return attr
    }

    static func buildExpression(_ expression: SpecialCharacters) -> Text {
        switch expression {
        case .comma:
            return Text(string: ",")
        case .space:
            return Text(string: " ")
        }
    }

    static func buildExpression(_ expression: Text) -> Text {
        expression
    }
}


enum SpecialCharacters {
    case space
    case comma
}

@AttributedStringBuilder
func greetBuilder(names: [String], title: String) -> Text {
    Text(string: "Hello ")
    for name in names {
        Text(string: name)
            .color(.red)
        SpecialCharacters.space
    }
    if !title.isEmpty {
        SpecialCharacters.comma
        Text(string:title)
        .font(.systemFont(ofSize: 20))
        .color(.blue)
    } else {
        SpecialCharacters.comma
        Text(string: "谢谢来访")
    }
}

greetBuilder(names: ["小华", "小明", "张三", "李四"], title: "欢迎来访👏🏻")

# 要点

  • buildBlock(_ components: Component...) -> Component 用来构建语句块的组合结果。每个结果构建器至少要提供一个它的具体实现。

  • buildOptional(_ component: Component?) -> Component 用于处理在特定执行中可能或不可能出现的部分结果。当一个结果构建器提供了 buildOptional(_😃 时,转译后的函数可以使用没有 else 的 if 语句,同时也提供了对 if let 的支持。

  • buildEither(first: Component) -> Component和buildEither(second: Component) -> Component 用于在选择语句的不同路径下建立部分结果。当一个结果构建器提供这两个方法的实现时,转译后的函数可以使用带有 else 的 if 语句以及 switch 语句。

  • buildArray(_ components: [Component]) -> Component 用来从一个循环的所有迭代中收集的部分结果。在一个结果构建器提供了 buildArray(_😃 的实现后,转译后的函数可以使用 for...in 语句。

  • buildExpression(_ expression: Expression) -> Component 它允许结果构建器区分表达式类型和组件类型,为语句表达式提供上下文类型信息。

  • buildFinalResult(_ component: Component) -> FinalResult 用于对最外层的 buildBlock 结果的再包装。例如,让结果构建器隐藏一些它并不想对外的类型(转换成可对外的类型)。

  • buildLimitedAvailability(_ component: Component) -> Component 用于将 buildBlock 在受限环境下(例如 if #available)产生的部分结果转化为可适合任何环境的结果,以提高 API 的兼容性。

# 参考

上次更新: 5/5/2022, 8:45:22 AM