Qu'est-ce qui active le DSL de SwiftUI?

88

Il semble que le nouveau SwiftUIcadre d' Apple utilise un nouveau type de syntaxe qui construit efficacement un tuple, mais a une autre syntaxe:

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

En essayant de comprendre ce qu'est vraiment cette syntaxe , j'ai découvert que l' VStackinitialiseur utilisé ici prend une fermeture du type () -> Content comme deuxième paramètre, où Contentest un paramètre générique conforme à Viewcelui qui est déduit via la fermeture. Pour savoir quel type Contentest déduit, j'ai légèrement modifié le code, en conservant ses fonctionnalités:

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

    return test
}

Avec cela, se testrévèle être de type VStack<TupleView<(Text, Text)>>, c'est-à- Contentdire de type TupleView<Text, Text>. En levant les yeux TupleView, j'ai trouvé que c'était un type de wrapper provenant de SwiftUIlui-même qui ne pouvait être initialisé qu'en passant le tuple qu'il devrait envelopper.

Question

Maintenant, je me demande comment dans le monde les deux Textinstances de cet exemple sont converties en un fichier TupleView<(Text, Text)>. Est-ce piraté SwiftUIet donc invalide la syntaxe Swift régulière? TupleViewêtre un SwiftUItype soutient cette hypothèse. Ou est-ce une syntaxe Swift valide? Si oui, comment peut-on l' utiliser à l'extérieur SwiftUI?

Fredpi
la source

Réponses:

109

Comme le dit Martin , si vous regardez la documentation de VStack's init(alignment:spacing:content:), vous pouvez voir que le content:paramètre a l'attribut @ViewBuilder:

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

Cet attribut fait référence au ViewBuildertype qui, si vous regardez l'interface générée, ressemble à:

@_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' @_functionBuilderattribut fait partie d'une fonctionnalité non officielle appelée " constructeurs de fonctions ", qui a été lancée ici sur l'évolution de Swift , et implémentée spécialement pour la version de Swift livrée avec Xcode 11, lui permettant d'être utilisée dans SwiftUI.

Le marquage d'un type @_functionBuilderpermet de l'utiliser comme attribut personnalisé sur diverses déclarations telles que des fonctions, des propriétés calculées et, dans ce cas, des paramètres de type de fonction. Ces déclarations annotées utilisent le générateur de fonctions pour transformer des blocs de code:

  • Pour les fonctions annotées, le bloc de code qui est transformé est l'implémentation.
  • Pour les propriétés calculées annotées, le bloc de code qui est transformé est le getter.
  • Pour les paramètres annotés de type fonction, le bloc de code qui est transformé est toute expression de fermeture qui lui est passée (le cas échéant).

La manière dont un générateur de fonctions transforme le code est définie par son implémentation de méthodes de générateur telles que buildBlock, qui prend un ensemble d'expressions et les consolide en une valeur unique.

Par exemple, ViewBuilderimplémente buildBlockde 1 à 10 Viewparamètres conformes, en consolidant plusieurs vues en une seule 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

    // ...
}

Cela permet à un ensemble d'expressions de vue dans une fermeture transmise à VStackl'initialiseur de être transformé en un appel à buildBlockqui prend le même nombre d'arguments. Par exemple:

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

se transforme en un appel à buildBlock(_:_:):

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

résultant en le type de résultat opaque some View satisfait par TupleView<(Text, Text)>.

Vous noterez que ViewBuilderne définit que buildBlockjusqu'à 10 paramètres, donc si nous essayons de définir 11 sous-vues:

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

nous obtenons une erreur du compilateur, car il n'y a pas de méthode de générateur pour gérer ce bloc de code (notez que, comme cette fonctionnalité est toujours en cours de travail, les messages d'erreur qui l'entourent ne seront pas très utiles).

En réalité, je ne pense pas que les gens se heurteront à cette restriction si souvent, par exemple, l'exemple ci-dessus serait mieux servi en utilisant la ForEachvue à la place:

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

Si toutefois vous avez besoin de plus de 10 vues définies statiquement, vous pouvez facilement contourner cette restriction en utilisant la Groupvue:

  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 implémente également d'autres méthodes de générateur de fonctions telles que:

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
}

Cela lui donne la possibilité de gérer les instructions if:

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

qui se transforme 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")
      )
    }
  }

(émettre des appels redondants à 1 argument ViewBuilder.buildBlockpour plus de clarté).

Hamish
la source
3
ViewBuilderne définit que buildBlockjusqu'à 10 paramètres - cela signifie-t-il var body: some Viewne pas pouvoir avoir plus de 11 sous-vues?
LinusGeffarth
1
@LinusGeffarth En réalité, je ne pense pas que les gens se heurteront à cette restriction si souvent, car ils voudront probablement utiliser quelque chose comme la ForEachvue à la place. Vous pouvez cependant utiliser la Groupvue pour contourner cette restriction, j'ai modifié ma réponse pour le montrer.
Hamish
3
@MandisaW - vous pouvez regrouper les vues dans vos propres vues et les réutiliser. Je n'y vois pas de problème. En fait, je suis à la WWDC en ce moment et j'ai parlé avec l'un des ingénieurs du laboratoire SwiftUI - il a dit que c'était une limitation de Swift pour le moment, et ils sont allés avec 10 comme nombre raisonnable. Une fois que les génériques variadiques seront introduits dans Swift, nous pourrons avoir autant de «sous-vues» que nous le souhaitons.
Losiowaty
1
Peut-être plus intéressant, quel est l'intérêt des méthodes buildEither? Il semble que vous deviez implémenter les deux, et les deux ont le même type de retour, pourquoi ne renvoient-ils pas chacun le type en question?
Gusutafu
13

Une chose analogue est décrite dans la vidéo Nouveautés de Swift WWDC dans la section sur les DSL (commence à ~ 31:15). L'attribut est interprété par le compilateur et traduit en code associé:

entrez la description de l'image ici

Maciek Czarnik
la source