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 能够被用在两种中央:
func
,var
或者subscript
的申明上,前提是这些申明不是某个协定须要的。- 作为一个函数的闭包参数,能够作为协定的一部分。
其实不管 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 扩大
最初展现成果如下:
最初,这里有老外做的一堆 awesome-function-builders,有依赖注入的,有 HTTP Request 的,有用来测试的等等,插个眼,有须要当前能够用