Was ermöglicht DSL von SwiftUI?

88

Es scheint, als ob Apples neues SwiftUIFramework eine neue Art von Syntax verwendet , die effektiv ein Tupel erstellt, aber eine andere Syntax hat:

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

Als ich versuchte herauszufinden, was diese Syntax wirklich ist , fand ich heraus, dass der VStackhier verwendete Initialisierer einen Abschluss des Typs () -> Content als zweiten Parameter verwendet, wobei Contentein generischer Parameter, der dem entspricht View, über den Abschluss abgeleitet wird. Um herauszufinden, auf welchen Typ geschlossen Contentwird, habe ich den Code geringfügig geändert und seine Funktionalität beibehalten:

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

    return test
}

Damit testoffenbart sich, dass es vom Typ ist VStack<TupleView<(Text, Text)>>, was bedeutet, dass Contentes vom Typ ist TupleView<Text, Text>. Als TupleViewich nach oben schaute, stellte ich fest, dass es sich um einen Wrapper-Typ handelt, der von sich SwiftUIselbst stammt und nur durch Übergeben des Tupels initialisiert werden kann, das umbrochen werden soll.

Frage

Jetzt frage ich mich, wie in Textaller Welt die beiden Instanzen in diesem Beispiel in a konvertiert werden TupleView<(Text, Text)>. Ist dies in die reguläre Swift-Syntax gehackt SwiftUIund daher ungültig? TupleViewein SwiftUITyp zu sein, unterstützt diese Annahme. Oder ist diese Swift-Syntax gültig? Wenn ja, wie kann man es draußen benutzen SwiftUI?

Fredpi
quelle

Antworten:

108

Wie Martin sagt , wenn Sie sich die Dokumentation für VStack's ansehen init(alignment:spacing:content:), können Sie sehen, dass der content:Parameter das Attribut hat @ViewBuilder:

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

Dieses Attribut bezieht sich auf den ViewBuilderTyp, der bei Betrachtung der generierten Schnittstelle folgendermaßen aussieht:

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

Das @_functionBuilderAttribut ist Teil einer inoffiziellen Funktion namens " Funktionsersteller ", die hier in der Swift-Evolution vorgestellt und speziell für die mit Xcode 11 gelieferte Swift-Version implementiert wurde, sodass sie in SwiftUI verwendet werden kann.

Durch Markieren eines Typs @_functionBuilderkann er als benutzerdefiniertes Attribut für verschiedene Deklarationen wie Funktionen, berechnete Eigenschaften und in diesem Fall Parameter des Funktionstyps verwendet werden. Solche mit Anmerkungen versehenen Deklarationen verwenden den Funktionsgenerator, um Codeblöcke zu transformieren:

  • Bei kommentierten Funktionen ist der Codeblock, der transformiert wird, die Implementierung.
  • Bei kommentierten berechneten Eigenschaften ist der zu transformierende Codeblock der Getter.
  • Bei mit Anmerkungen versehenen Parametern vom Funktionstyp ist der zu transformierende Codeblock ein beliebiger Abschlussausdruck, der an ihn übergeben wird (falls vorhanden).

Die Art und Weise, wie ein Funktions-Builder Code transformiert, wird durch die Implementierung von Builder-Methoden definiert, z. B. buildBlockdie eine Reihe von Ausdrücken verwenden und sie zu einem einzigen Wert zusammenfassen.

Zum Beispiel ViewBuilderArbeitsgeräte buildBlockfür 1 bis 10 Viewentsprechen , Parameter, mehrere Ansichten zu einem einzigen Konsolidierung 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

    // ...
}

Auf diese Weise kann eine Reihe von Ansichtsausdrücken innerhalb eines an den VStackInitialisierer übergebenen Abschlusses in einen Aufruf umgewandelt werden buildBlock, der dieselbe Anzahl von Argumenten benötigt. Zum Beispiel:

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

verwandelt sich in einen Aufruf an buildBlock(_:_:):

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

was dazu führt, dass der undurchsichtige Ergebnistyp some View erfüllt wird durch TupleView<(Text, Text)>.

Sie werden feststellen, dass ViewBuildernur buildBlockbis zu 10 Parameter definiert werden. Wenn wir also versuchen, 11 Unteransichten zu definieren:

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

Wir erhalten einen Compilerfehler, da es keine Builder-Methode gibt, um diesen Codeblock zu verarbeiten (beachten Sie, dass die Fehlermeldungen nicht so hilfreich sind, da diese Funktion noch in Arbeit ist).

In Wirklichkeit glaube ich nicht, dass Menschen so oft auf diese Einschränkung stoßen werden, zum Beispiel wäre das obige Beispiel besser, wenn man stattdessen die ForEachAnsicht verwendet:

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

Wenn Sie jedoch mehr als 10 statisch definierte Ansichten benötigen, können Sie diese Einschränkung mithilfe der GroupAnsicht leicht umgehen :

  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 implementiert auch andere Funktionserstellungsmethoden wie:

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
}

Dies gibt ihm die Möglichkeit, if-Anweisungen zu verarbeiten:

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

was verwandelt wird 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")
      )
    }
  }

(Das Aussenden redundanter 1-Argument-Aufrufe ViewBuilder.buildBlockerfordert Klarheit).

Hamish
quelle
3
ViewBuilderdefiniert nur buildBlockbis zu 10 Parameter - bedeutet das, dass var body: some Viewnicht mehr als 11 Unteransichten vorhanden sein können?
LinusGeffarth
1
@LinusGeffarth In Wirklichkeit glaube ich nicht, dass die Leute so oft auf diese Einschränkung stoßen werden, da sie wahrscheinlich stattdessen so etwas wie die ForEachAnsicht verwenden möchten . Sie können die GroupAnsicht jedoch verwenden , um diese Einschränkung zu umgehen. Ich habe meine Antwort bearbeitet, um dies zu zeigen.
Hamish
3
@MandisaW - Sie können Ansichten in Ihren eigenen Ansichten gruppieren und wiederverwenden. Ich sehe kein Problem damit. Ich bin gerade bei WWDC und habe mit einem der Ingenieure im SwiftUI-Labor gesprochen - er sagte, dass es momentan eine Einschränkung von Swift ist, und sie haben 10 als vernünftige Zahl gewählt. Sobald verschiedene generische Generika in Swift eingeführt wurden, können wir so viele „Unteransichten“ haben, wie wir möchten.
Losiowaty
1
Vielleicht interessanter, was ist der Sinn der buildEither-Methoden? Es scheint, als müssten Sie beide implementieren, und beide haben denselben Rückgabetyp. Warum geben sie nicht jeweils nur den betreffenden Typ zurück?
Gusutafu
1
Nach meinem Kommentar zum ASTPrinter-Fehler wird dieser Fehler auf dem Master behoben, sobald der PR des Funktionserstellers zusammengeführt wurde .
Hamish
13

Eine analoge Sache wird in Was ist neu in Swift WWDC-Video im Abschnitt über DSLs beschrieben (beginnt um ~ 31: 15). Das Attribut wird vom Compiler interpretiert und in verwandten Code übersetzt:

Geben Sie hier die Bildbeschreibung ein

Maciek Czarnik
quelle