Function Builder 是 Swift 5.1 引入的特性,大大增强了 Swift 语言构建内置 DSL 的能力。SwiftUI 声明式 UI 构建方式就是靠的 DSL 实现的。


从 DSL 说起

DSL 是 Domain Specific Language 的缩写,意思就是特定领域的语言。与之对应的就是我们熟悉的 C, Java, Swift 这些通用的语言,通用语言什么领域都可以去插一脚,无非就是适不适合,好不好用罢了。DSL 则是局限在某个特定场景的特别设计过的语言,因为专一,所以专业,它们往往能以非常轻量级的语法和易于理解的方式来解决特定问题。

举几个著名的 DSL 的例子:

  • 正则表达式

通过一些规定好的符号和组合规则,通过正则表达式引擎来实现字符串的匹配

  • HTML & CSS

虽然写的是类似XML 或者 .{} 一样的字符规则,但是最终都会被浏览器内核转变成Dom树,从而渲染到Webview上

  • SQL

诸如 create select insert 这种单词后面跟上参数,这样的语句实现了对数据库的增删改查一系列程序工作

那么这种语言内建的 DSL 有什么好处呢,我们先来看一个 HTML 的界面搭建:

<div>
  <p>Hello World!</p>
  <p>My name is KY!</p>
</div>

在 UIKit 里要搭建上述界面很明显要麻烦很多:

let container = UIStackView()
container.axis = .vertical
container.distribution = .equalSpacing

let paragraph1 = UILabel()
paragraph1.text = "Hello, World!"

let paragraph2 = UILabel()
paragraph2.text = "My name is KY!"

container.addSubview(paragraph1)
container.addSubview(paragraph2)

这就是声明式 UI 与 命令式 UI 的区别,声明式 UI 用 DSL 来描述 “UI 应该是什么样子的”,命令式 UI 则需要先创建一个 View,再指定这个 View 的特性,再指定这个 View 要放到哪里,一步一步来,显得较为笨重。

DSL 可以让 SwiftUI 以类似 HTML 的方式来搭建界面

构建 SwiftUI 的 DSL 语法的除了 Function Builder 还有 Property Wrapper, Opaque Return Type,链式调用 等特性,本文只讨论 Function Builder


Function Builder

Function Builder 本质上是语法糖,没有 Function Builder, Swift 依然可以构建 DSL,但是会麻烦不少。不用 Function Builder 来构建 DSL 可以参看这篇文章:Building DSLs in Swift

下面,跟随 Function Builders in Swift and SwiftUI 这篇文章通过构建一个 AttributedStringBuilder 来学会 FunctionBuilder (翻译)


理解 Function Builder

一个 Function Builder 是一个类型,它实现了一个内置的 DSL,这个 DSL 可以把一个函数内的表达式作为部分结果(partial results)收集起来合并成返回值

最小的 Function Builder 类型实现如下:

@_functionBuilder struct Builder {
    static func buildBlock(_ partialResults: String...) -> String {
        partialResults.reduce("", +)
    }
}

定义 Function Builder 要用 @_functionBuilder 来修饰,定义好之后就可以作为 Attribute 来用了。

注意下划线,表示这个功能还没有被正式采纳,仍然在开发中,以后可能会有变化

这个静态的 buildBlock() 方法是必须的。

一个 function builder attribute 可以被用在两种地方:

  1. func, var 或者 subscript 的声明上,前提是这些声明不是某个协议需要的。
  2. 作为一个函数的闭包参数,可以作为协议的一部分。

其实不论 function builder 用在哪里,它都是将跟在后面的表达式串作为参数传递给它的 buildBlock() 方法,用该方法的返回值就是被标注的实际值

让我们用上面定义的 @Builder 作为例子来体会下这两种使用场景:

用在声明里代码如下:

@Builder func abc() -> String {
    "Method: "
    "ABC"
}

struct Foo {
    @Builder var abc: String {
        "Getter: "
        "ABC"
    }
    
    @Builder subscript(_ anything: String) -> String {
        "sbscript"
        "ABC"
    }
}

用在闭包参数上代码如下:

func acceptBuilder(@Builder _ builder: () -> String) -> Void {
    print(builder())
}

运行测试代码:

func testBuilder() -> Void {
    print(abc())
    print(Foo().abc)
    print(Foo()[""])
    acceptBuilder {
        "Closure Argument: "
        "ABC "
    }
}

打印内容如下:

Method: ABC
Getter: ABC
Subscript: ABC
Closure Argument: ABC

funcion builder 要解决的问题就是构造多层次的异构数据结构。举两个例子来说就是:

  • 生成结构数据,如 XML, JSON 等
  • 生成 GUI 层次结构,如 SwiftUI, HTML 等

这就是 function builder 要做的事,那它是怎么工作的呢?


深入 Function Builder

如果我们将方法 abc() 生成的 AST dump 出来可以看到:

(func_decl range=[builder.swift:10:10 - line:13:1] "abc()" interface type='() -> String' access=internal
...
  (declref_expr implicit type='(Builder.Type) -> (String...) -> String' location=builder.swift:10:31 range=[builder.swift:10:31 - line:10:31] decl=builder.(file).Builder.buildBlock@builder.swift:5:17 function_ref=single)
  ...
    (string_literal_expr type='String' location=builder.swift:11:5 range=[builder.swift:11:5 - line:11:5] encoding=utf8 value="Method: " builtin_initializer=Swift.(file).String extension.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:) initializer=**NULL**)
    (string_literal_expr type='String' location=builder.swift:12:5 range=[builder.swift:12:5 - line:12:5] encoding=utf8 value="ABC" builtin_initializer=Swift.(file).String extension.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:) initializer=**NULL**)
...

我们可以发现最后调用的其实是:

Builder.buildBlock("Method: ", "ABC")

语法分析(semantic analysis)阶段,Swift 编译器会将function builder transforms这个东西 applies 到 parsed AST 上,就好像我们已经写了 Builder.buildBlock(<arguments>) 一样 (1, 2)

另一个例子是 function builder 用作闭包参数的时候。在这种情况下,Swift 编译器会 rewrite the closure to a closure with a single expression body containing the builder invocations.

在某些情况下,一个 function builder 需要提供下面这几个 building 方法来满足不同类型的变形(transformations)需要(1, 2):

  • buildBlock(_ parts: PartialResult...) -> PartialResult

将部分结果聚合成一个

  • ​buildDo(_ parts: PartialResult...) -> PartialResult

与 ​​buildBlock()​ 一样,只是作用于 ​​do​ 语句

  • ​buildIf(_ part: PartialResult?) -> PartialResult

作用于 ​​if​ 语句,true 时 ​​part 为后面跟的内容转换成的 ​​PartialResult​,false时 ​​part​ 为 ​​nil

  • ​buildEither(first: PartialResult) -> PartialResult​ 与 ​​buildEither(second: PartialResult) -> PartialResult

作用于 ​​if...else...​ 语句,必须同时实现

  • ​buildExpression(_ expression: Expression) -> PartialResult

把单个的非 ​​PartialResult​ 转换成 ​​PartialResult

  • ​buildOptional(_ part: PartialResult?) -> PartialResult

将一个可空 ​​PartialResult​ 转换成不可空的

  • ​buildFinalResult(_ parts: PartialResult...) -> Result

将多个 ​​PartialResult​ 转换成 ​​Result

所有这些方法都支持基于其参数类型的 overloads

所以呢,Swift 编译器在碰到 function builder 的时候会用上述方法来替换 DSL 语法内容。如果找不到相应的方法,就会报编译错误


实现定制的 Function Builder

让我们实现一个 ​​NSAttributedString​ 的 function builder 吧,代码如下:

@_functionBuilder struct AttributedStringBuilder {
    // 基本方法
    static func buildBlock(_ parts: NSAttributedString...) -> NSAttributedString {
        let result = NSMutableAttributedString(string: "")
        parts.forEach(result.append)
        return result
    }
    
    // String 转成 NSAttributedString
    static func buildExpression(_ text: String) -> NSAttributedString {
        NSAttributedString(string: text)
    }
    
    // 转 UIImage
    static func buildExpression(_ image: UIImage) -> NSAttributedString {
        NSAttributedString(attachment: NSTextAttachment(image: image))
    }
    
    // 转自己,不是很清楚为什么一定要这个方法,感觉有上面几个就够了呀,但是实践上没有这个会报错
    static func buildExpression(_ attrString: NSAttributedString) -> NSAttributedString {
        attrString
    }
    
    // 支持 if 语句
    static func buildIf(_ attrString: NSAttributedString?) -> NSAttributedString {
        attrString ?? NSAttributedString()
    }
    
    // 支持 if/else 语句
    static func buildEither(first: NSAttributedString) -> NSAttributedString {
        first
    }
    static func buildEither(second: NSAttributedString) -> NSAttributedString {
        second
    }
}

为了用起来,还需要一个添加 attributes 的方法和用这个 builder 的便利构造器:

extension NSAttributedString {
    // 帮助加 Attributes
    func withAttributes(_ attrs: [NSAttributedString.Key : Any]) -> NSAttributedString {
        let result = NSMutableAttributedString(attributedString: self)
        result.addAttributes(attrs, range: NSRange(location: 0, length: self.length))
        return result
    }
    
    // 以 DSL 方式来初始化
    convenience init(@AttributedStringBuilder builder: () -> NSAttributedString) {
        self.init(attributedString: builder())
    }
}

接下来我们要来测试一下这个新的 NSAttributedString,因为 NSAttributedString 是 UIKit 的,所以我们得把它放在 UILabel 里,再用 ​​UIViewRepresentable​ 包装一下才能用在 SwiftUI 里:

struct AttributedStringRepresentable: UIViewRepresentable {
    
    let attrbutedString: NSAttributedString
    
    func makeUIView(context: Context) -> UILabel {
        let label = UILabel()
        label.numberOfLines = 0
        label.attributedText = attrbutedString
        return label
    }
    
    func updateUIView(_ uiView: UILabel, context: Context) { }
}

SwiftUI 测试代码:

struct AttributedStringView: View {
    let optional = true
    
    var body: some View {
        AttributedStringRepresentable(
            attrbutedString: NSAttributedString {
                NSAttributedString {
                    "Folder"
                    UIImage(systemName: "folder")!
                }
                NSAttributedString { }
                "\n"
                NSAttributedString {
                    "Document"
                    UIImage(systemName: "doc")!
                }
                .withAttributes([
                    .font : UIFont.systemFont(ofSize: 32),
                    .foregroundColor : UIColor.red
                ])
                "\n"
                "Blue One".foregroundColor(.blue)
                    .background(.gray)
                    .underline(.cyan)
                    .font(UIFont.systemFont(ofSize: 20))
                "\n"
                if optional {
                    NSAttributedString {
                        "Hello "
                            .foregroundColor(.red)
                            .font(UIFont.systemFont(ofSize: 10.0))
                          "World"
                            .foregroundColor(.green)
                            .underline(.orange, style: .thick)
                    }
                    UIImage(systemName: "rays")!
                }
                "\n"
                if optional {
                    "It's True".foregroundColor(.magenta)
                        .font(UIFont.systemFont(ofSize: 28))
                } else {
                    "It's False".foregroundColor(.purple)
                }
            }
        )
        .frame(width: 250, height: 250)
    }
}

上面代码里的 .foregroundColor 这些来自于 String 和 NSAttributedString 的 modifiers 扩展

最后展示效果如下:
image

最后,这里有老外做的一堆 awesome-function-builders,有依赖注入的,有HTTP Request的,有用来测试的等等,插个眼,有需要以后可以用


firerainky
4 声望2 粉丝