Co umożliwia DSL SwiftUI?

88

Wygląda na to, że nowa SwiftUIstruktura Apple wykorzystuje nowy rodzaj składni, która skutecznie buduje krotkę, ale ma inną składnię:

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

Próbując wyjaśnić, czym naprawdę jest ta składnia , dowiedziałem się, że VStackużyty tutaj inicjator przyjmuje zamknięcie typu () -> Content jako drugi parametr, gdzie Contentjest to ogólny parametr zgodny z Viewtym, który jest wywnioskowany przez zamknięcie. Aby dowiedzieć się, do jakiego typu Contentjest wywnioskowany, nieznacznie zmieniłem kod, zachowując jego funkcjonalność:

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

    return test
}

Dzięki temu testokazuje się być typem VStack<TupleView<(Text, Text)>>, co oznacza, że Contentjest typu TupleView<Text, Text>. Patrząc w górę TupleView, stwierdziłem, że jest to typ otoki pochodzący od SwiftUIsamego siebie, który można zainicjować tylko przez przekazanie krotki, którą ma zawijać.

Pytanie

Teraz zastanawiam się, jak na świecie te dwa Textwystąpienia w tym przykładzie są konwertowane na TupleView<(Text, Text)>. Czy to się włamało, SwiftUIa zatem jest nieprawidłową zwykłą składnią Swift? TupleViewbycie SwiftUItypem wspiera to założenie. A może jest to poprawna składnia Swift? Jeśli tak, jak można go używać na zewnątrz SwiftUI?

fredpi
źródło

Odpowiedzi:

108

Jak mówi Martin , jeśli spojrzysz na dokumentację dla VStack's init(alignment:spacing:content:), zobaczysz, że content:parametr ma atrybut @ViewBuilder:

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

Ten atrybut odnosi się do ViewBuildertypu, który patrząc na wygenerowany interfejs wygląda następująco:

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

@_functionBuilderAtrybut jest częścią nieoficjalną funkcję o nazwie „ budowniczych funkcyjne ”, który został dwuspadowym o Swift ewolucji tutaj i realizowane specjalnie dla wersji Swift że statki z Xcode 11, co pozwala na stosowanie go w SwiftUI.

Oznaczenie typu @_functionBuilderpozwala na użycie go jako atrybutu niestandardowego w różnych deklaracjach, takich jak funkcje, obliczone właściwości i, w tym przypadku, parametry typu funkcji. Takie deklaracje z adnotacjami używają konstruktora funkcji do przekształcania bloków kodu:

  • W przypadku funkcji z adnotacjami implementacją jest blok kodu, który jest przekształcany.
  • W przypadku obliczonych właściwości z adnotacjami blok kodu, który jest transformowany, jest funkcją pobierającą.
  • W przypadku parametrów typu funkcji z adnotacjami blok kodu, który jest transformowany, to każde przekazane do niego wyrażenie zamykające (jeśli istnieje).

Sposób, w jaki konstruktor funkcji przekształca kod, jest definiowany przez implementację metod konstruktora, takich jak buildBlock, która pobiera zestaw wyrażeń i konsoliduje je w jedną wartość.

Na przykład ViewBuilderimplementuje buildBlockod 1 do 10 Viewzgodnych parametrów, konsolidując wiele widoków w jeden 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

    // ...
}

Pozwala to VStackna przekształcenie zestawu wyrażeń widoku w zamknięciu przekazanym do inicjalizatora w wywołanie, buildBlockktóre pobiera taką samą liczbę argumentów. Na przykład:

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

zmienia się w połączenie z buildBlock(_:_:):

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

w wyniku czego nieprzezroczysty typ wyniku some View zostanie spełniony przez TupleView<(Text, Text)>.

Zauważysz, że ViewBuilderdefiniuje tylko buildBlockdo 10 parametrów, więc jeśli spróbujemy zdefiniować 11 podglądów podrzędnych:

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

otrzymujemy błąd kompilatora, ponieważ nie ma metody konstruktora do obsługi tego bloku kodu (zwróć uwagę, że ponieważ ta funkcja jest nadal w toku, komunikaty o błędach wokół niej nie będą tak pomocne).

W rzeczywistości nie sądzę, aby ludzie często napotykali na to ograniczenie, na przykład powyższy przykład byłby lepszy przy użyciu ForEachwidoku:

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

Jeśli jednak potrzebujesz więcej niż 10 statycznie zdefiniowanych widoków, możesz łatwo obejść to ograniczenie za pomocą Groupwidoku:

  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 implementuje również inne metody konstruktora funkcji, takie jak:

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
}

Daje to możliwość obsługi instrukcji if:

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

który zostaje przekształcony w:

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

(emitowanie zbędnych 1-argumentowych wezwań do ViewBuilder.buildBlockjasności).

Hamish
źródło
3
ViewBuilderdefiniuje tylko buildBlockdo 10 parametrów - czy to oznacza, że var body: some Viewnie może mieć więcej niż 11 podglądów podrzędnych?
LinusGeffarth
1
@LinusGeffarth W rzeczywistości nie sądzę, żeby ludzie często napotykali to ograniczenie, ponieważ prawdopodobnie będą chcieli użyć czegoś takiego jak ForEachwidok. Możesz jednak użyć Groupwidoku, aby obejść to ograniczenie, zredagowałem moją odpowiedź, aby to pokazać.
Hamish
3
@MandisaW - możesz grupować widoki we własne widoki i wykorzystywać je ponownie. Nie widzę z tym problemu. Jestem teraz w WWDC i rozmawiałem z jednym z inżynierów z laboratorium SwiftUI - powiedział, że teraz jest to ograniczenie Swift, a oni wybrali 10 jako rozsądną liczbę. Gdy zmienna generyczna zostanie wprowadzona do Swift, będziemy mogli mieć tyle „podglądów podrzędnych”, ile chcemy.
Losiowaty
1
Może bardziej interesujące, jaki jest cel budowy obu metod? Wygląda na to, że musisz zaimplementować oba i oba mają ten sam typ zwrotu, dlaczego nie zwracają po prostu danego typu?
Gusutafu
1
W związku z moim komentarzem na temat błędu ASTPrinter, zostanie to naprawione na serwerze głównym po scaleniu kodu PR budowniczego funkcji .
Hamish
13

Analogiczna rzecz jest opisana w filmie Co nowego w Swift WWDC w sekcji o DSL (zaczyna się od ~ 31:15). Atrybut jest interpretowany przez kompilator i tłumaczony na powiązany kod:

wprowadź opis obrazu tutaj

Maciek Czarnik
źródło