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 = .verticalcontainer.distribution = .equalSpacinglet 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: ABCGetter: ABCSubscript: ABCClosure 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时 partnil

  • buildEither(first: PartialResult) -> PartialResultbuildEither(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 扩大

最初展现成果如下:

最初,这里有老外做的一堆 awesome-function-builders,有依赖注入的,有HTTP Request的,有用来测试的等等,插个眼,有须要当前能够用