¿Qué habilita el DSL de SwiftUI?

88

Parece que el nuevo SwiftUImarco de Apple usa un nuevo tipo de sintaxis que efectivamente construye una tupla, pero tiene otra sintaxis:

var body: some View {
    VStack(alignment: .leading) {
        Text("Hello, World") // No comma, no separator ?!
        Text("Hello World!")
    }
}

Al tratar de abordar lo que realmente es esta sintaxis , descubrí que el VStackinicializador que se usa aquí toma un cierre del tipo () -> Content como segundo parámetro, donde Contenthay un parámetro genérico conforme al Viewque se infiere a través del cierre. Para saber de qué tipo Contentse infiere, modifiqué ligeramente el código, manteniendo su funcionalidad:

var body: some View {
    let test = VStack(alignment: .leading) {
        Text("Hello, World")
        Text("Hello World!")
    }

    return test
}

Con esto, se testrevela ser de tipo VStack<TupleView<(Text, Text)>>, es decir, que Contentes de tipo TupleView<Text, Text>. Mirando hacia arriba TupleView, descubrí que es un tipo de contenedor que se origina en SwiftUIsí mismo y que solo se puede inicializar pasando la tupla que debería ajustar.

Pregunta

Ahora me pregunto cómo en el mundo las dos Textinstancias de este ejemplo se convierten a TupleView<(Text, Text)>. ¿Se ha pirateado SwiftUIy, por lo tanto, la sintaxis regular de Swift no es válida? TupleViewser un SwiftUItipo apoya esta suposición. ¿O es esta sintaxis Swift válida? Si es así, ¿cómo se puede usar en el exterior SwiftUI?

fredpi
fuente

Respuestas:

108

Como dice Martin , si miras la documentación de VStack's init(alignment:spacing:content:), puedes ver que el content:parámetro tiene el atributo @ViewBuilder:

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

Este atributo se refiere al ViewBuildertipo, que si observa la interfaz generada, se ve así:

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

El @_functionBuilderatributo es parte de una característica no oficial llamada " constructores de funciones ", que se ha incluido en Swift evolution aquí e implementado especialmente para la versión de Swift que se envía con Xcode 11, lo que permite su uso en SwiftUI.

Marcar un tipo @_functionBuilderpermite que se utilice como un atributo personalizado en varias declaraciones como funciones, propiedades calculadas y, en este caso, parámetros del tipo de función. Tales declaraciones anotadas usan el generador de funciones para transformar bloques de código:

  • Para las funciones anotadas, el bloque de código que se transforma es la implementación.
  • Para las propiedades calculadas anotadas, el bloque de código que se transforma es el captador.
  • Para los parámetros anotados del tipo de función, el bloque de código que se transforma es cualquier expresión de cierre que se le pasa (si corresponde).

La forma en que un constructor de funciones transforma el código se define mediante la implementación de métodos de construcción como buildBlock, que toma un conjunto de expresiones y las consolida en un solo valor.

Por ejemplo, ViewBuilderimplementa buildBlockde 1 a 10 Viewparámetros conformes, consolidando múltiples vistas en una sola 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

    // ...
}

Esto permite que un conjunto de expresiones de vista dentro de un cierre pasado al VStackinicializador se transforme en una llamada a buildBlockque toma el mismo número de argumentos. Por ejemplo:

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

se transforma en una llamada a buildBlock(_:_:):

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

dando como resultado que el tipo de resultado opaco some View sea ​​satisfecho por TupleView<(Text, Text)>.

Observará que ViewBuildersolo define buildBlockhasta 10 parámetros, por lo que si intentamos definir 11 subvistas:

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

obtenemos un error de compilador, ya que no hay un método de compilador para manejar este bloque de código (tenga en cuenta que debido a que esta característica aún está en proceso, los mensajes de error que la rodean no serán tan útiles).

En realidad, no creo que las personas se encuentren con esta restricción con tanta frecuencia, por ejemplo, el ejemplo anterior se serviría mejor usando la ForEachvista:

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

Sin embargo, si necesita más de 10 vistas definidas estáticamente, puede solucionar fácilmente esta restricción utilizando la Groupvista:

  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 también implementa otros métodos de creación de funciones como:

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
}

Esto le da la capacidad de manejar declaraciones if:

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

que se transforma en:

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

(emitiendo llamadas redundantes de 1 argumento ViewBuilder.buildBlockpara mayor claridad).

Hamish
fuente
3
ViewBuildersolo define buildBlockhasta 10 parámetros ; ¿eso significa que var body: some Viewno puede tener más de 11 subvistas?
LinusGeffarth
1
@LinusGeffarth En realidad, no creo que la gente se encuentre con esta restricción con tanta frecuencia, ya que probablemente querrán usar algo como la ForEachvista en su lugar. Sin embargo, puede usar la Groupvista para solucionar esta restricción, he editado mi respuesta para mostrar eso.
Hamish
3
@MandisaW: puede agrupar vistas en sus propias vistas y reutilizarlas. No veo ningún problema con eso. En realidad, estoy en WWDC ahora mismo y hablé con uno de los ingenieros del laboratorio de SwiftUI; dijo que es una limitación de Swift en este momento, y eligieron 10 como un número razonable. Una vez que se introduzca el genérico variadic en Swift, podremos tener tantas "subvistas" como queramos.
Losiowaty
1
Quizás más interesante, ¿cuál es el punto de los métodos buildEither? Parece que necesita implementar ambos, y ambos tienen el mismo tipo de retorno, ¿por qué no devuelven cada uno el tipo en cuestión?
Gusutafu
13

Una cosa análoga se describe en el video What's New in Swift WWDC en la sección sobre DSL (comienza en ~ 31: 15). El compilador interpreta el atributo y lo traduce al código relacionado:

ingrese la descripción de la imagen aquí

Maciek Czarnik
fuente