是什么启用了 SwiftUI 的 DSL?

看起来苹果新的 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)>>类型,这意味着 ContentTupleView<Text, Text>类型。查找 TupleView,我发现它是一个源自 SwiftUI本身的包装类型,只能通过传递它应该包装的元组来初始化。

提问

现在我想知道这个例子中的两个 Text实例是如何转换成 TupleView<(Text, Text)>的。这是否被分解成 SwiftUI,因此 无效的正规 Swift 语法? TupleViewSwiftUI类型支持这个假设。或者是这个 有效的 Swift 语法?如果是的,怎么可能一个 SwiftUI之外使用?

11055 次浏览

As Martin says, if you look at the documentation for VStack's init(alignment:spacing:content:), you can see that the content: parameter has the attribute @ViewBuilder:

init(alignment: HorizontalAlignment = .center, spacing: Length? = nil,
@ViewBuilder content: () -> Content)

This attribute refers to the ViewBuilder type, which if you look at the generated interface, looks like:

@_functionBuilder 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
}

The @_functionBuilder attribute is a part of an unofficial feature called "function builders", which has been pitched on Swift evolution here, and implemented specially for the version of Swift that ships with Xcode 11, allowing it to be used in SwiftUI.

Marking a type @_functionBuilder allows it to be used as a custom attribute on various declarations such as functions, computed properties and, in this case, parameters of function type. Such annotated declarations use the function builder to transform blocks of code:

  • For annotated functions, the block of code that gets transformed is the implementation.
  • For annotated computed properties, the block of code that gets transformed is the getter.
  • For annotated parameters of function type, the block of code that gets transformed is any closure expression that is passed to it (if any).

The way in which a function builder transforms code is defined by its implementation of builder methods such as buildBlock, which takes a set of expressions and consolidates them into a single value.

For example, ViewBuilder implements buildBlock for 1 to 10 View conforming parameters, consolidating multiple views into a single 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


// ...
}

This allows a set of view expressions within a closure passed to VStack's initialiser to be transformed into a call to buildBlock that takes the same number of arguments. For example:

struct ContentView : View {
var body: some View {
VStack(alignment: .leading) {
Text("Hello, World")
Text("Hello World!")
}
}
}

gets transformed into a call to buildBlock(_:_:):

struct ContentView : View {
var body: some View {
VStack(alignment: .leading) {
ViewBuilder.buildBlock(Text("Hello, World"), Text("Hello World!"))
}
}
}

resulting in the opaque result type some View being satisfied by TupleView<(Text, Text)>.

You'll note that ViewBuilder only defines buildBlock up to 10 parameters, so if we attempt to define 11 subviews:

  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!")
}
}

we get a compiler error, as there's no builder method to handle this block of code (note that because this feature is still a work-in-progress, the error messages around it won't be that helpful).

In reality, I don't believe people will run into this restriction all that often, for example the above example would be better served using the ForEach view instead:

  var body: some View {
VStack(alignment: .leading) {
ForEach(0 ..< 20) { i in
Text("Hello world \(i)")
}
}
}

If however you do need more than 10 statically defined views, you can easily workaround this restriction using the Group view:

  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 also implements other function builder methods such:

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
}

This gives it the ability to handle if statements:

  var body: some View {
VStack(alignment: .leading) {
if .random() {
Text("Hello World!")
} else {
Text("Goodbye World!")
}
Text("Something else")
}
}

which gets transformed into:

  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")
)
}
}

(emitting redundant 1-argument calls to ViewBuilder.buildBlock for clarity).

An analogous thing is described in What's New in Swift WWDC video in the section about DSLs (starts at ~31:15). The attribute is interpreted by the compiler and translated into related code:

enter image description here