一、前言
结果生成器(以前叫做函数生成器)是 Swift5.4 中引入的一项新 feature,它是 SwiftUI 中支持 ViewBuilder 的技术。随着 Xcode12.5 的发布,苹果正式向开发者开放了它,允许我们为各种用例创建自己的自定义结果生成器。 本文讲讲解结果生成器的基本概念、工作原理以及如何使用它来创建自己的自定义结果生成器。
二、基本形式
作为演示,我们创建一个字符串生成器,并使用 ⭐️ 作为分隔符,例如,给定“Hello”和“World”,字符串生成器将返回一个连接的字符串“Hello”⭐️“World”。 开始使用结果生成器的最基本形式来构建字符串生成器:
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 func buildStringFunc ( ) - > String {
}
@StringBuilder var buildStringVar: String {
}
注意上面提到的组件区域,它是向 StringBuilder 提供所需字符串的地方,components 区域中的每一行表示 buildBlock(_: ) 可变参数的一个组件。如下所示,以 StringBuilder 为例:
@StringBuilder func greet ( ) - > String {
"Hello"
"World"
}
print ( greet ( ) )
func greetTranslated ( ) - > String {
let finalOutput = StringBuilder . buildBlock ( "Hello" , "World" )
return finalOutput
}
print ( greetTranslated ( ) )
三、选择语句
① 没有 else 块的 if 语句
假设要扩展 greet() 方法的功能,接受 name 参数,然后根据 name 来跟用户打招呼,可以这样更新 greet() 方法:
@StringBuilder func greet ( name: String ) - > String {
"Hello"
"World"
if ! name. isEmpty {
"to"
name
}
}
print ( greet ( name: "Swift 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 {
var partialComponent1: String ?
if ! name. isEmpty {
partialComponent1 = StringBuilder . buildBlock ( "to" , name)
}
let partialComponent2 = StringBuilder . buildOptional ( partialComponent1)
let finalOutput = StringBuilder . buildBlock ( "Hello" , "World" , partialComponent2)
return finalOutput
}
print ( greetTranslated ( name: "Swift Senpai" ) )
注意结果生成器是如何首先解析 if 块中的任何内容,然后递归地传递和解析部分组件,直到它获得最终输出的。此行为非常重要,因为它从根本上演示了结果生成器如何解析 components 区域中的所有组件(添加 buildOptional(_: ) 方法不仅支持没有 else 块的 if 语句,还支持可选绑定)。 此时,如果尝试使用空的 name 调用 greet(name: ) 函数,将得到以下输出:
print ( greet ( name: "" ) )
输出字符串的末尾额外的"⭐️",是由于 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" ) )
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 {
let partialComponent1 = StringBuilder . buildBlock ( "to" , name)
partialComponent2 = StringBuilder . buildEither ( first : partialComponent1)
} else {
let partialComponent1 = StringBuilder . buildBlock ( "World" )
partialComponent2 = StringBuilder . buildEither ( second: partialComponent1)
}
let finalOutput = StringBuilder . buildBlock ( "Hello" , partialComponent2)
return finalOutput
}
print ( greetTranslated ( name: "Swift Senpai" ) )
四、for-in 循环
接下来更新 greet(name: ) 函数,如下所示:
@StringBuilder func greet ( name: String , countdown: Int ) - > String {
for i in ( 0 . . . countdown) . reversed ( ) {
"\( i) "
}
"Hello"
if ! name. isEmpty {
"to"
name
} else {
"World"
}
}
print ( greet ( name: "Swift Senpai" , countdown: 5 ) )
注意,在函数的开头添加了一个倒计时参数和 for 循环,for 循环将执行从给定值到 0 的倒计时。下一步也是最后一步是使用以下结果生成方法更新 StringBuilder:
static func buildArray ( _ components: [ String ] ) - > String {
return components. joined ( separator: "" )
}
请注意,buildArray(: )方法与结果生成方法的其余部分稍有不同,它将数组作为输入。在场景后面发生的是,在每次迭代结束时,for 循环将生成一个字符串(部分组件)。在经历了所有迭代之后,每个迭代的结果将被分组为一个数组,并将其传递给 buildArray( : ) 方法。为了更好地说明流程,下面是等效函数:
func greetTranslated ( name: String , countdown: Int ) - > String {
var partialComponents = [ String ] ( )
for i in ( 0 . . . countdown) . reversed ( ) {
let component = StringBuilder . buildBlock ( "\( i) " )
partialComponents. append ( component)
}
let loopComponent = StringBuilder . buildArray ( partialComponents)
let finalOutput = StringBuilder . buildBlock ( loopComponent, "Hello" , partialComponent2)
return finalOutput
}
print ( greetTranslated ( name: "Swift Senpai" , countdown: 5 ) )
有了它,StringBuilder 就能够处理 for-in 循环,再试着运行代码,会看到在 Xcode 控制台打印"543210⭐️Hello⭐️to⭐️Swift Senpai"。 注:添加 buildArray(_: ) 方法将不支持 while 循环。实际上,for-in 循环是结果生成器支持的唯一循环方法。
五、支持不同的数据类型
在这个阶段,我们已经使 StringBuilder 非常灵活,它现在可以接受选择语句、for 循环和可选绑定作为输入。但是,有一个很大的限制:它只能支持字符串作为输入和输出数据类型。幸运的是,支持各种输入和输出数据类型非常简单。
① 启用各种输入数据类型
假设想让 StringBuilder 支持 Int 作为输入类型,可以将以下结果构建方法添加到 StringBuilder 中:
static func buildExpression ( _ expression: Int ) - > String {
return "\( expression) "
}
此 buildExpression(: ) 方法是可选的,它接受整型作为输入并返回字符串。一旦实现,它将成为结果生成器的入口点,并充当适配器,将其输入数据类型转换为 buildBlock(: )方法接受的数据类型。 这就是为什么会看到多个“Cannot convert value of type’String’to expected argument type’Int’”错误出现在添加 buildExpression(:_ ) 方法之后,StringBuilder 现在不再接受 String 作为输入数据类型,而是接受 Int 作为输入数据类型。幸运的是,可以在 StringBuilder 中实现多个 buildExpression(:_) 方法,使其同时接受 String 和 Int 输入数据类型。继续并添加以下实现,它将使所有错误消失:
static func buildExpression ( _ expression: String ) - > String {
return expression
}
有了这两种方法,现在可以更改 greet(name:countdown:)函数的 for 循环,如下所示,所有内容仍将相应地工作:
@StringBuilder func greet ( name: String , countdown: Int ) - > String {
for i in ( 0 . . . countdown) . reversed ( ) {
i
}
}
print ( greet ( name: "Swift Senpai" , countdown: 5 ) )
② 启用各种输出数据类型
添加对各种输出数据类型的支持也相当容易。它的工作原理类似于支持各种输入数据类型,但这次必须实现 buildFinalResult(_: ) 方法,该方法在最终输出之前添加一个额外的处理层。出于演示目的,让 StringBuilder 能够输出一个整数,表示最终输出的字符串字符数:
static func buildFinalResult ( _ component: String ) - > Int {
return component. count
}
同时确保实现以下最终结果方法,这样 StringBuilder 就不会失去输出字符串的能力:
static func buildFinalResult ( _ component: String ) - > String {
return component
}
要查看所有操作,可以创建 Int 类型的 StringBuilder 变量:
@StringBuilder var greetCharCount: Int {
"Hello"
"World"
}
print ( greetCharCount)
六、结果生成器用例