什么使SwiftUI的DSL成为可能?


88

苹果的新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



Answers:


108

正如马丁说,如果你看的文件VStackinit(alignment:spacing:content:),你可以看到content:参数具有的属性@ViewBuilder

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

此属性引用ViewBuilder类型,如果您查看生成的接口,则该类型类似于:

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

@_functionBuilder属性是一个非官方功能的一部分,该功能称为“功能构建器”,在此处针对Swift演变进行了介绍,并专门针对Xcode 11随附的Swift版本实现,从而可在SwiftUI中使用。

标记类型@_functionBuilder允许将其用作各种声明(例如函数,计算属性以及在这种情况下为函数类型的参数)上的自定义属性。此类带注释的声明使用函数构建器来转换代码块:

  • 对于带注释的函数,要转换的代码块是实现。
  • 对于带注释的计算属性,要转换的代码块是getter。
  • 对于带功能类型的带注释的参数,要转换的代码块是传递给它的任何闭包表达式(如果有)。

函数构建器转换代码的方式是由其构建器方法的实现定义的,例如buildBlock采用一组表达式并将其合并为单个值。

例如,ViewBuilder实现buildBlock1到10个View符合参数的实现,将多个视图合并为一个视图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只能定义buildBlock10个参数,因此,如果我们尝试定义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!")
    }
  }

我们会收到一个编译器错误,因为没有构建器方法可以处理此代码块(请注意,由于此功能仍在开发中,因此围绕它的错误消息将无济于事)。

实际上,我认为人们不会经常遇到这种限制,例如,使用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")
      )
    }
  }

(发出冗余的1个参数要求ViewBuilder.buildBlock明确)。


3
ViewBuilderbuildBlock最多只能定义10个参数-这是否意味着var body: some View不能有超过11个子视图?
LinusGeffarth '19

1
@LinusGeffarth实际上,我认为人们不会经常遇到这种限制,因为他们可能想使用诸如ForEach视图之类的东西。但是,您可以使用Group视图解决此限制,我已经编辑了答案以显示该限制。
Hamish

3
@MandisaW-您可以将视图分为自己的视图,然后重用它们。我没有发现任何问题。实际上,我现在在WWDC上,并且与SwiftUI实验室的一位工程师进行了交谈-他说这是Swift的局限性,他们选择了10作为明智的数字。一旦可变参数泛型被引入到Swift中,我们将能够拥有任意数量的“子视图”。
Losiowaty19年

1
也许更有趣,buildEither方法的意义是什么?似乎您需要同时实现这两者,并且两者都具有相同的返回类型,为什么它们不都只是返回所讨论的类型呢?
Gusutafu

1
跟进我对ASTPrinter错误的评论,一旦合并功能构建器PR错误将在master上修复
Hamish

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.