1
前言
Preface
结果生成器(以前叫做函数生成器)是swift5.4
中引入的一项新feature
,它是SwiftUI
中支持ViewBuilder
的技术。随着Xcode12.5的发布(目前处于beta测试阶段),苹果正式向开发者开放了它,允许我们为各种用例创建自己的自定义结果生成器。
本文讲讲解结果生成器的基本概念
、工作原理
以及如何使用它来创建自己的自定义结果生成器
。
话不多说,让我们马上开始吧!
2
基本形式
The Basic Form
作为演示,我们创建一个字符串生成器
,并使用⭐️
作为分隔符。例如,给定“Hello”
和“World”
,我们的字符串生成器将返回一个连接的字符串“Hello”⭐️“World”
。
让我们开始使用结果生成器的最基本形式来构建字符串生成器:
代码语言:javascript复制resultBuilder
struct StringBuilder {
static func buildBlock(_ components: String...) -> String {
return components.joined(separator: "")
}
}
你可以通过使用@resultBuilder
属性标记自定义结构体,并强制实现buildBlock(_:)
静态方法来定义结果生成器。
buildBlock(_:)
方法类似于StringBuilder
的入口点,它接受组件的可变参数,这意味着它可以是1个或多个字符串。在buildBlock(_:)
方法中,我们可以对给定的组件进行任何处理。在这个例子中,我们将使用 "⭐️"
作为分隔符。
在实现buildBlock(_:)
方法时,需要遵循一条规则:返回的数据类型必须与components
数据类型匹配。以StringBuilder
为例,buildBlock(_:)
方法组件是String
类型的,因此其返回类型也必须是String
。
要创建StringBuilder
实例,可以使用@StringBuilder
标记函数或变量:
// 用 `StringBuilder`标记函数
@StringBuilder func buildStringFunc() -> String {
// components区域
// ...
}
// 用 `StringBuilder`标记变量
@StringBuilder var buildStringVar: String {
// components区域
// ...
}
注意上面提到的组件区域,它是向StringBuilder
提供所需字符串的地方。components区域
中的每一行表示buildBlock(_:)
可变参数的一个组件。以下面的StringBuilder
为例:
@StringBuilder func greet() -> String {
"Hello"
"World"
}
print(greet())
// Output: "HelloWorld"
可以翻译为:
代码语言:javascript复制func greetTranslated() -> String {
//解析StringBuilder中的所有部分组件`
let finalOutput = StringBuilder.buildBlock("Hello", "World")
return finalOutput
}
print(greetTranslated())
小Tip:
您可以在buildBlock(_:)
方法中添加print
语句,以查看何时触发它以及在任何给定时间提供了哪些组件。
这就是创建结果生成器
所需的全部内容。现在您已经看到了一个基本的结果生成器,让我们继续向StringBuilder
添加更多的功能。
3
选择语句
The Selection Statements
没有“else”块的“if”语句
假设我们要扩展greet()
方法的功能,接受name
参数然后根据name
来跟用户打招呼。我们可以这样更新greet()
方法:
@StringBuilder func greet(name: String) -> String {
"Hello"
"World"
if !name.isEmpty {
"to"
name
}
}
print(greet(name: "Swift Senpai"))
// Expected output: "HelloWorldtoSwift Senpai"
这样修改以后,你应该会看到编译器开始抱怨:
Closure containing control flow statement cannot be used with result builder 'StringBuilder' 包含控制流语句的闭包不能与结果生成器“StringBuilder”一起使用
这是因为我们的StringBuilder
目前不理解什么是if语句
。为了支持没有else
的if语句
,我们必须将以下结果构建方法添加到StringBuilder
中。
@resultBuilder
struct StringBuilder {
// ...
// ...
static func buildOptional(_ component: String?) -> String {
return component ?? ""
}
}
它的工作原理是,当满足if语句
条件时,把部分结果传递给buildOptional(_:)
方法,否则把nil
传递给buildOptional(_:)
方法。
为了让你更清楚地了解结果生成器是如何解析覆盖下的每个部分组件,上面的greet(name:)
函数等效于以下代码段:
func greetTranslated(name: String) -> String {
// Resolve all partial components within the `if` block
var partialComponent1: String?
if !name.isEmpty {
partialComponent1 = StringBuilder.buildBlock("to", name)
}
// Resolve the entire `if` block
let partialComponent2 = StringBuilder.buildOptional(partialComponent1)
// Resolve all partial components in `StringBuilder`
let finalOutput = StringBuilder.buildBlock("Hello", "World", partialComponent2)
return finalOutput
}
print(greetTranslated(name: "Swift Senpai"))
// Output: "HelloWorldtoSwift Senpai"
注意结果生成器是如何首先解析if块
中的任何内容,然后递归地传递和解析部分组件,直到它获得最终输出的。此行为非常重要,因为它从根本上演示了结果生成器如何解析components
区域中的所有组件。
小Tip:
添加buildOptional(_:)
方法不仅支持没有else块的if语句
,还支持可选绑定。
此时,如果尝试使用空的name
调用greet(name:)
函数,将得到以下输出:
print(greet(name: ""))
// Actual output: HelloWorld
// Expected output: HelloWorld
输出字符串的末尾额外的"⭐️"
,是由于buildBlock(_:)
方法通过buildOptional(_:)
方法连接空字符串返回。为了解决这个问题,我们可以简单地更新buildBlock(_:)
方法,在连接之前过滤掉组件中的所有空字符串:
static func buildBlock(_ components: String...) -> String {
let filtered = components.filter { $0 != "" }
return filtered.joined(separator: "")
}
带有“else”块的“if”语句
我们的StringBuilder
现在比以前更聪明了,但是说“Hello⭐️World⭐️to⭐️“Swift Senpai”
听起来怪怪的。
让我们把它变得更聪明,当name不为空时它就可以输出"Hello⭐️to⭐️[name]"
,否则输出 "Hello⭐️World"
。继续更新greet(name:)
函数,如下所示:
@StringBuilder func greet(name: String) -> String {
"Hello"
if !name.isEmpty {
"to"
name
} else {
"World"
}
}
print(greet(name: "Swift Senpai"))
// Expected output: "HellotoSwift Senpai"
您将再次看到编译错误:
Closure containing control flow statement cannot be used with result builder 'StringBuilder' 包含控制流语句的闭包不能与结果生成器“StringBuilder”一起使用
这一次,由于额外的else块
,我们必须实现另外两种结果构建方法:
static func buildEither(first component: String) -> String {
return component
}
static func buildEither(second component: String) -> String {
return component
}
这两种方法总是结合在一起的。当满足if块
条件时,buildery(first:)
方法将触发;然而,当满足else块
条件时,buildery(second:)
方法将触发。下面是一个等价函数,可以帮助您理解场景背后发生的逻辑:
func greetTranslated(name: String) -> String {
var partialComponent2: String!
if !name.isEmpty {
// Resolve all partial components within the `if` block
let partialComponent1 = StringBuilder.buildBlock("to", name)
// Resolve the entire `if-else` block
partialComponent2 = StringBuilder.buildEither(first: partialComponent1)
} else {
// Resolve all partial components within the `else` block
let partialComponent1 = StringBuilder.buildBlock("World")
// Resolve the entire `if-else` block
partialComponent2 = StringBuilder.buildEither(second: partialComponent1)
}
// Resolve all partial components in `StringBuilder`
let finalOutput = StringBuilder.buildBlock("Hello", partialComponent2)
return finalOutput
}
print(greetTranslated(name: "Swift Senpai"))
// Output: "HellotoSwift Senpai"
4
for-in循环
The “for-in” Loops
接下来,让我们更新greet(name:)
函数,在问候用户之前倒计时,因为为什么不呢?