是什么启用了 SwiftUI 的 DSL?
What enables SwiftUI's DSL?
Apple 的新 SwiftUI
框架似乎使用了一种 新的语法 可以有效地构建元组,但有另一种语法:
var body: some View {
VStack(alignment: .leading) {
Text("Hello, World") // No comma, no separator ?!
Text("Hello World!")
}
}
试图弄清楚这个语法到底是什么,我发现这里使用的 VStack
初始化器采用了 () -> Content
类型的闭包
作为第二个参数,其中 Content
是一个符合 View
的通用参数,它是通过闭包推断出来的。为了找出 Content
被推断为什么类型,我稍微更改了代码,保持其功能:
var body: some View {
let test = VStack(alignment: .leading) {
Text("Hello, World")
Text("Hello World!")
}
return test
}
有了这个,test
显示自己是 VStack<TupleView<(Text, Text)>>
类型,这意味着 Content
是 TupleView<Text, Text>
类型。查找 TupleView
,我发现它是一个源自 SwiftUI
本身的包装器类型,只能通过传递它应该包装的元组来初始化。
问题
现在我想知道这个示例中的两个 Text
实例到底是如何转换为 TupleView<(Text, Text)>
的。这是否侵入了 SwiftUI
,因此 无效的常规 Swift 语法? TupleView
作为 SwiftUI
类型支持这个假设。或者这是有效的Swift语法吗?如果是,如何在SwiftUI
之外使用它?
, if you look at the documentation for VStack
's init(alignment:spacing:content:)
,可以看到content:
参数有属性@ViewBuilder
:
init(alignment: HorizontalAlignment = .center, spacing: Length? = nil,
<b>@ViewBuilder</b> content: () -> Content)
这个属性指的是 ViewBuilder
类型,如果你查看生成的界面,它看起来像:
<b>@_functionBuilder</b> public struct ViewBuilder {
/// Builds an empty view from an block containing no statements, `{ }`.
public static func buildBlock() -> EmptyView
/// Passes a single view written as a child view (e..g, `{ Text("Hello") }`)
/// through unmodified.
public static func buildBlock(_ content: Content) -> Content
where Content : View
}
@_functionBuilder
属性是名为“function builders", which has been pitched on Swift evolution here 的非官方功能的一部分,专门为 Xcode 11 附带的 Swift 版本实现,允许它将用于 SwiftUI。
标记类型 @_functionBuilder
允许它用作各种声明的自定义属性,例如函数、计算属性以及在本例中为函数类型的参数。这种带注释的声明使用函数构建器来转换代码块:
- 对于带注释的函数,转换的代码块是实现。
- 对于带注释的计算属性,转换的代码块是 getter。
- 对于函数类型的注释参数,转换的代码块是传递给它的任何闭包表达式(如果有的话)。
函数生成器转换代码的方式由其 builder methods 的实现定义,例如 buildBlock
,它采用一组表达式并将它们合并为一个值。
例如,ViewBuilder
为 1 到 10 个 View
符合参数实施 buildBlock
,将多个视图合并为一个 TupleView
:
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {
/// Passes a single view written as a child view (e..g, `{ Text("Hello") }`)
/// through unmodified.
public static func buildBlock<Content>(_ content: Content)
-> Content where Content : View
public static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1)
-> TupleView<(C0, C1)> where C0 : View, C1 : View
public static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2)
-> TupleView<(C0, C1, C2)> where C0 : View, C1 : View, C2 : View
// ...
}
这允许将传递给 VStack
的初始化程序的闭包中的一组视图表达式转换为对 buildBlock
的调用,该调用采用相同数量的参数。例如:
struct ContentView : View {
var body: some View {
VStack(alignment: .leading) {
Text("Hello, World")
Text("Hello World!")
}
}
}
转换为对 buildBlock(_:_:)
:
的调用
struct ContentView : View {
var body: some View {
VStack(alignment: .leading) {
ViewBuilder.buildBlock(Text("Hello, World"), Text("Hello World!"))
}
}
}
导致 some View
被 TupleView<(Text, Text)>
满足。
你会注意到 ViewBuilder
只定义了 buildBlock
最多 10 个参数,所以如果我们试图定义 11 个子视图:
var body: some View {
// error: Static member 'leading' cannot be used on instance of
// type 'HorizontalAlignment'
VStack(alignment: .leading) {
Text("Hello, World")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
}
}
我们收到编译器错误,因为没有构建器方法来处理此代码块(请注意,因为此功能仍然是 work-in-progress,围绕它的错误消息不会有帮助)。
实际上,我不相信人们会经常 运行 进入这个限制,例如上面的例子使用 ForEach
视图会更好:
var body: some View {
VStack(alignment: .leading) {
ForEach(0 ..< 20) { i in
Text("Hello world \(i)")
}
}
}
如果您确实需要超过 10 个静态定义的视图,您可以使用 Group
视图轻松解决此限制:
var body: some View {
VStack(alignment: .leading) {
Group {
Text("Hello world")
// ...
// up to 10 views
}
Group {
Text("Hello world")
// ...
// up to 10 more views
}
// ...
}
ViewBuilder
还实现了其他函数生成器方法,例如:
extension ViewBuilder {
/// Provides support for "if" statements in multi-statement closures, producing
/// ConditionalContent for the "then" branch.
public static func buildEither<TrueContent, FalseContent>(first: TrueContent)
-> ConditionalContent<TrueContent, FalseContent>
where TrueContent : View, FalseContent : View
/// Provides support for "if-else" statements in multi-statement closures,
/// producing ConditionalContent for the "else" branch.
public static func buildEither<TrueContent, FalseContent>(second: FalseContent)
-> ConditionalContent<TrueContent, FalseContent>
where TrueContent : View, FalseContent : View
}
这使它能够处理 if 语句:
var body: some View {
VStack(alignment: .leading) {
if .random() {
Text("Hello World!")
} else {
Text("Goodbye World!")
}
Text("Something else")
}
}
转换为:
var body: some View {
VStack(alignment: .leading) {
ViewBuilder.buildBlock(
.random() ? ViewBuilder.buildEither(first: Text("Hello World!"))
: ViewBuilder.buildEither(second: Text("Goodbye World!")),
Text("Something else")
)
}
}
(为清楚起见,发出对 ViewBuilder.buildBlock
的冗余 1 参数调用)。
What's New in Swift WWDC video 在有关 DSL 的部分(从 ~31:15 开始)中描述了类似的事情。该属性由编译器解释并翻译成相关代码:
Apple 的新 SwiftUI
框架似乎使用了一种 新的语法 可以有效地构建元组,但有另一种语法:
var body: some View {
VStack(alignment: .leading) {
Text("Hello, World") // No comma, no separator ?!
Text("Hello World!")
}
}
试图弄清楚这个语法到底是什么,我发现这里使用的 VStack
初始化器采用了 () -> Content
类型的闭包
作为第二个参数,其中 Content
是一个符合 View
的通用参数,它是通过闭包推断出来的。为了找出 Content
被推断为什么类型,我稍微更改了代码,保持其功能:
var body: some View {
let test = VStack(alignment: .leading) {
Text("Hello, World")
Text("Hello World!")
}
return test
}
有了这个,test
显示自己是 VStack<TupleView<(Text, Text)>>
类型,这意味着 Content
是 TupleView<Text, Text>
类型。查找 TupleView
,我发现它是一个源自 SwiftUI
本身的包装器类型,只能通过传递它应该包装的元组来初始化。
问题
现在我想知道这个示例中的两个 Text
实例到底是如何转换为 TupleView<(Text, Text)>
的。这是否侵入了 SwiftUI
,因此 无效的常规 Swift 语法? TupleView
作为 SwiftUI
类型支持这个假设。或者这是有效的Swift语法吗?如果是,如何在SwiftUI
之外使用它?
VStack
's init(alignment:spacing:content:)
,可以看到content:
参数有属性@ViewBuilder
:
init(alignment: HorizontalAlignment = .center, spacing: Length? = nil,
<b>@ViewBuilder</b> content: () -> Content)
这个属性指的是 ViewBuilder
类型,如果你查看生成的界面,它看起来像:
<b>@_functionBuilder</b> public struct ViewBuilder {
/// Builds an empty view from an block containing no statements, `{ }`.
public static func buildBlock() -> EmptyView
/// Passes a single view written as a child view (e..g, `{ Text("Hello") }`)
/// through unmodified.
public static func buildBlock(_ content: Content) -> Content
where Content : View
}
@_functionBuilder
属性是名为“function builders", which has been pitched on Swift evolution here 的非官方功能的一部分,专门为 Xcode 11 附带的 Swift 版本实现,允许它将用于 SwiftUI。
标记类型 @_functionBuilder
允许它用作各种声明的自定义属性,例如函数、计算属性以及在本例中为函数类型的参数。这种带注释的声明使用函数构建器来转换代码块:
- 对于带注释的函数,转换的代码块是实现。
- 对于带注释的计算属性,转换的代码块是 getter。
- 对于函数类型的注释参数,转换的代码块是传递给它的任何闭包表达式(如果有的话)。
函数生成器转换代码的方式由其 builder methods 的实现定义,例如 buildBlock
,它采用一组表达式并将它们合并为一个值。
例如,ViewBuilder
为 1 到 10 个 View
符合参数实施 buildBlock
,将多个视图合并为一个 TupleView
:
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {
/// Passes a single view written as a child view (e..g, `{ Text("Hello") }`)
/// through unmodified.
public static func buildBlock<Content>(_ content: Content)
-> Content where Content : View
public static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1)
-> TupleView<(C0, C1)> where C0 : View, C1 : View
public static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2)
-> TupleView<(C0, C1, C2)> where C0 : View, C1 : View, C2 : View
// ...
}
这允许将传递给 VStack
的初始化程序的闭包中的一组视图表达式转换为对 buildBlock
的调用,该调用采用相同数量的参数。例如:
struct ContentView : View {
var body: some View {
VStack(alignment: .leading) {
Text("Hello, World")
Text("Hello World!")
}
}
}
转换为对 buildBlock(_:_:)
:
struct ContentView : View {
var body: some View {
VStack(alignment: .leading) {
ViewBuilder.buildBlock(Text("Hello, World"), Text("Hello World!"))
}
}
}
导致 some View
被 TupleView<(Text, Text)>
满足。
你会注意到 ViewBuilder
只定义了 buildBlock
最多 10 个参数,所以如果我们试图定义 11 个子视图:
var body: some View {
// error: Static member 'leading' cannot be used on instance of
// type 'HorizontalAlignment'
VStack(alignment: .leading) {
Text("Hello, World")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
}
}
我们收到编译器错误,因为没有构建器方法来处理此代码块(请注意,因为此功能仍然是 work-in-progress,围绕它的错误消息不会有帮助)。
实际上,我不相信人们会经常 运行 进入这个限制,例如上面的例子使用 ForEach
视图会更好:
var body: some View {
VStack(alignment: .leading) {
ForEach(0 ..< 20) { i in
Text("Hello world \(i)")
}
}
}
如果您确实需要超过 10 个静态定义的视图,您可以使用 Group
视图轻松解决此限制:
var body: some View {
VStack(alignment: .leading) {
Group {
Text("Hello world")
// ...
// up to 10 views
}
Group {
Text("Hello world")
// ...
// up to 10 more views
}
// ...
}
ViewBuilder
还实现了其他函数生成器方法,例如:
extension ViewBuilder {
/// Provides support for "if" statements in multi-statement closures, producing
/// ConditionalContent for the "then" branch.
public static func buildEither<TrueContent, FalseContent>(first: TrueContent)
-> ConditionalContent<TrueContent, FalseContent>
where TrueContent : View, FalseContent : View
/// Provides support for "if-else" statements in multi-statement closures,
/// producing ConditionalContent for the "else" branch.
public static func buildEither<TrueContent, FalseContent>(second: FalseContent)
-> ConditionalContent<TrueContent, FalseContent>
where TrueContent : View, FalseContent : View
}
这使它能够处理 if 语句:
var body: some View {
VStack(alignment: .leading) {
if .random() {
Text("Hello World!")
} else {
Text("Goodbye World!")
}
Text("Something else")
}
}
转换为:
var body: some View {
VStack(alignment: .leading) {
ViewBuilder.buildBlock(
.random() ? ViewBuilder.buildEither(first: Text("Hello World!"))
: ViewBuilder.buildEither(second: Text("Goodbye World!")),
Text("Something else")
)
}
}
(为清楚起见,发出对 ViewBuilder.buildBlock
的冗余 1 参数调用)。
What's New in Swift WWDC video 在有关 DSL 的部分(从 ~31:15 开始)中描述了类似的事情。该属性由编译器解释并翻译成相关代码: