Cosa abilita il DSL di SwiftUI?


89

Sembra che il nuovo SwiftUIframework di Apple utilizzi un nuovo tipo di sintassi che costruisce efficacemente una tupla, ma ha un'altra sintassi:

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

Cercando di capire cosa sia realmente questa sintassi , ho scoperto che l' VStackinizializzatore qui utilizzato accetta una chiusura del tipo () -> Content come secondo parametro, dove Contentè un parametro generico conforme a Viewquello dedotto tramite la chiusura. Per scoprire a quale tipo Contentsi deduce, ho modificato leggermente il codice, mantenendone la funzionalità:

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

    return test
}

Con questo, test rivela di tipo VStack<TupleView<(Text, Text)>>, nel senso che Contentè di tipo TupleView<Text, Text>. Guardando in alto TupleView, ho scoperto che è un tipo di wrapper originato da SwiftUIse stesso che può essere inizializzato solo passando la tupla che dovrebbe avvolgere.

Domanda

Ora mi chiedo come nel mondo le due Textistanze in questo esempio vengano convertite in un file TupleView<(Text, Text)>. La sintassi regolare di Swift è stata compromessa SwiftUIe quindi non valida? TupleViewessere un SwiftUItipo supporta questa ipotesi. O è questa valida sintassi Swift? Se sì, come usarlo all'esterno SwiftUI?



Risposte:


111

Come dice Martin , se guardi la documentazione per VStack's init(alignment:spacing:content:), puoi vedere che il content:parametro ha l'attributo @ViewBuilder:

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

Questo attributo si riferisce al ViewBuildertipo, che se guardi l'interfaccia generata, ha il seguente aspetto:

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

L' @_functionBuilderattributo fa parte di una funzionalità non ufficiale chiamata " costruttori di funzioni ", che è stata presentata qui su Swift evolution e implementata appositamente per la versione di Swift fornita con Xcode 11, permettendone l'utilizzo in SwiftUI.

Contrassegnare un tipo ne @_functionBuilderconsente l'utilizzo come attributo personalizzato su varie dichiarazioni come funzioni, proprietà calcolate e, in questo caso, parametri del tipo di funzione. Tali dichiarazioni annotate utilizzano il generatore di funzioni per trasformare blocchi di codice:

  • Per le funzioni annotate, il blocco di codice che viene trasformato è l'implementazione.
  • Per le proprietà calcolate annotate, il blocco di codice che viene trasformato è il getter.
  • Per i parametri annotati del tipo di funzione, il blocco di codice che viene trasformato è qualsiasi espressione di chiusura che gli viene passata (se presente).

Il modo in cui un generatore di funzioni trasforma il codice è definito dalla sua implementazione di metodi di compilazione come buildBlock, che prende un insieme di espressioni e le consolida in un unico valore.

Ad esempio, ViewBuilderimplementa buildBlockda 1 a 10 Viewparametri conformi, consolidando più viste in una singola 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

    // ...
}

Ciò consente di VStacktrasformare un insieme di espressioni di visualizzazione all'interno di una chiusura passata all'inizializzatore di S in una chiamata a buildBlockche accetta lo stesso numero di argomenti. Per esempio:

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

si trasforma in una chiamata a buildBlock(_:_:):

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

con il risultato che il tipo di risultato opaco some View viene soddisfatto da TupleView<(Text, Text)>.

Noterai che ViewBuilderdefinisce solo buildBlockfino a 10 parametri, quindi se proviamo a definire 11 sottoview:

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

otteniamo un errore del compilatore, poiché non esiste un metodo di creazione per gestire questo blocco di codice (si noti che poiché questa funzione è ancora in corso, i messaggi di errore attorno ad essa non saranno così utili).

In realtà, non credo che le persone si imbatteranno in questa restrizione così spesso, ad esempio l'esempio sopra sarebbe meglio servito utilizzando ForEachinvece la vista:

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

Se tuttavia hai bisogno di più di 10 viste definite staticamente, puoi facilmente aggirare questa restrizione utilizzando 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 implementa anche altri metodi di creazione di funzioni come:

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
}

Questo gli dà la capacità di gestire le istruzioni if:

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

che si trasforma in:

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

(emettendo chiamate ridondanti a 1 argomento ViewBuilder.buildBlockper chiarezza).


4
ViewBuilderdefinisce solo buildBlockfino a 10 parametri - significa che var body: some Viewnon può avere più di 11 sottoview?
LinusGeffarth

1
@LinusGeffarth In realtà non penso che le persone si imbatteranno in questa restrizione così spesso, poiché probabilmente vorranno utilizzare qualcosa come la ForEachvista. Puoi tuttavia utilizzare la Groupvista per aggirare questa restrizione, ho modificato la mia risposta per dimostrarlo.
Hamish

3
@ MandisaW: puoi raggruppare le visualizzazioni nelle tue visualizzazioni e riutilizzarle. Non vedo alcun problema con esso. In questo momento sono al WWDC e ho parlato con uno degli ingegneri del laboratorio SwiftUI: ha detto che è una limitazione di Swift in questo momento, e hanno scelto 10 come numero ragionevole. Una volta che il generico variadico verrà introdotto in Swift, saremo in grado di avere tutte le "sottoview" che vogliamo.
Losiowaty

1
Forse più interessante, qual è il punto dei metodi buildEither? Sembra che sia necessario implementare entrambi ed entrambi hanno lo stesso tipo di ritorno, perché non restituiscono semplicemente il tipo in questione?
Gusutafu,


13

Una cosa analoga è descritta nel video Novità di Swift WWDC nella sezione sui DSL (inizia a ~ 31: 15). L'attributo viene interpretato dal compilatore e tradotto nel relativo codice:

inserisci qui la descrizione dell'immagine

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.