O que ativa o DSL do SwiftUI?

88

Parece que a nova SwiftUIestrutura da Apple usa um novo tipo de sintaxe que efetivamente constrói uma tupla, mas tem outra sintaxe:

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

Tentando descobrir o que essa sintaxe realmente é , descobri que o VStackinicializador usado aqui leva um encerramento do tipo () -> Content como o segundo parâmetro, onde Contenté um parâmetro genérico em conformidade com o Viewque é inferido por meio do encerramento. Para descobrir a que tipo Contenté inferido, mudei ligeiramente o código, mantendo sua funcionalidade:

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

    return test
}

Com isso, testrevela-se do tipo VStack<TupleView<(Text, Text)>>, ou seja, que Contenté do tipo TupleView<Text, Text>. Olhando para cima TupleView, descobri que é um tipo de wrapper originado de SwiftUIsi mesmo que só pode ser inicializado passando a tupla que deve envolver.

Questão

Agora estou me perguntando como no mundo as duas Textinstâncias neste exemplo são convertidas em um TupleView<(Text, Text)>. Isso é hackeado SwiftUIe, portanto, sintaxe Swift regular inválida? TupleViewser um SwiftUItipo apóia essa suposição. Ou esta é uma sintaxe Swift válida? Se sim, como usá-lo fora SwiftUI?

Fredpi
fonte

Respostas:

108

Como Martin diz , se você olhar a documentação de VStack's init(alignment:spacing:content:), verá que o content:parâmetro tem o atributo @ViewBuilder:

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

Este atributo se refere ao ViewBuildertipo, que se você olhar para a interface gerada, é semelhante a:

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

O @_functionBuilderatributo é parte de um recurso não oficial chamado " construtores de função ", que foi lançado na evolução do Swift aqui , e implementado especialmente para a versão do Swift que acompanha o Xcode 11, permitindo que seja usado no SwiftUI.

A marcação de um tipo @_functionBuilderpermite que ele seja usado como um atributo personalizado em várias declarações, como funções, propriedades calculadas e, neste caso, parâmetros do tipo de função. Essas declarações anotadas usam o construtor de função para transformar blocos de código:

  • Para funções anotadas, o bloco de código que é transformado é a implementação.
  • Para propriedades computadas anotadas, o bloco de código que é transformado é o getter.
  • Para parâmetros anotados do tipo de função, o bloco de código que é transformado é qualquer expressão de encerramento que é passada para ele (se houver).

A maneira pela qual um construtor de função transforma o código é definida por sua implementação de métodos de construtor , como buildBlock, que pega um conjunto de expressões e as consolida em um único valor.

Por exemplo, ViewBuilderimplementa buildBlockde 1 a 10 Viewparâmetros em conformidade, consolidando várias visualizações em uma única 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

    // ...
}

Isso permite que um conjunto de expressões de exibição dentro de um encerramento passado ao VStackinicializador de seja transformado em uma chamada para buildBlockque leva o mesmo número de argumentos. Por exemplo:

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

é transformado em uma chamada para buildBlock(_:_:):

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

resultando no tipo de resultado opaco some View sendo satisfeito por TupleView<(Text, Text)>.

Você notará que ViewBuilderdefine apenas buildBlockaté 10 parâmetros, portanto, se tentarmos definir 11 subvisualizações:

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

obtemos um erro do compilador, pois não há nenhum método do construtor para lidar com esse bloco de código (observe que, como esse recurso ainda é um trabalho em andamento, as mensagens de erro em torno dele não serão tão úteis).

Na realidade, não acredito que as pessoas vão se deparar com essa restrição com tanta frequência, por exemplo, o exemplo acima seria melhor servido usando a ForEachvisualização:

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

No entanto, se você precisar de mais de 10 visualizações definidas estaticamente, pode facilmente contornar essa restrição usando a Groupvisualização:

  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 também implementa outros métodos construtores de função, 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
}

Isso dá a ele a capacidade de lidar com declarações if:

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

que se transforma em:

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

(emitindo chamadas redundantes de 1 argumento para ViewBuilder.buildBlockpara maior clareza).

Hamish
fonte
3
ViewBuilderdefine apenas buildBlockaté 10 parâmetros - isso significa que var body: some Viewnão pode ter mais de 11 subvisualizações?
LinusGeffarth
1
@LinusGeffarth Na realidade, não acho que as pessoas vão se deparar com essa restrição com tanta frequência, já que provavelmente vão querer usar algo como a ForEachvisualização. No entanto, você pode usar a Groupvisualização para contornar essa restrição, editei minha resposta para mostrar isso.
Hamish
3
@MandisaW - você pode agrupar visualizações em suas próprias visualizações e reutilizá-las. Não vejo problema nisso. Estou na WWDC agora, na verdade, e conversei com um dos engenheiros do laboratório SwiftUI - ele disse que é uma limitação do Swift agora, e eles escolheram 10 como um número sensato. Uma vez que o genérico variadic seja introduzido no Swift, seremos capazes de ter quantas “subvisualizações” quisermos.
Losiowaty
1
Talvez mais interessante, qual é o objetivo dos métodos buildEither? Parece que você precisa implementar ambos, e ambos têm o mesmo tipo de retorno, por que não retornam apenas o tipo em questão?
Gusutafu
1
Seguindo meu comentário sobre o bug ASTPrinter, isso será corrigido no master assim que os construtores de função PR forem mesclados .
Hamish de
13

Uma coisa análoga é descrita em O que há de novo no vídeo Swift WWDC na seção sobre DSLs (começa em ~ 31:15). O atributo é interpretado pelo compilador e traduzido no código relacionado:

insira a descrição da imagem aqui

Maciek Czarnik
fonte