La magie de SwiftUI ou sur les constructeurs de fonctions


Avez-vous essayé d'ajouter plus de 10 vues à VStack?


 var body: some View { VStack { Text("Placeholder1") Text("Placeholder2") // ...    3  10 . . . Text("Placeholder11") } } 

J'ai essayé - il ne compile pas. Oui, j'ai également été surpris au début et plongé dans l'étude du forum Swift et de github. Le résultat de mon étude était - "il ne compile toujours pas ¯\_(ツ)_/¯ ". Mais attendez, voyons pourquoi.


Constructeur de fonctions


Pour commencer, il vaut la peine de comprendre comment une telle syntaxe est devenue généralement disponible. La base d'une telle création déclarative d'éléments si inhabituels pour nous est le mécanisme Function Builder .
Sur le github en évolution rapide, il y a une proposition de John McCall et Doug Gregor - Function builders (Proposition: SE-XXXX) , dans laquelle ils décrivent en détail le problème auquel ils étaient confrontés, pourquoi il a été décidé d'utiliser le générateur de fonctions et de quoi il s'agissait. tel.


Alors qu'est-ce que c'est?


Il est difficile de décrire cela en un mot, mais en bref - c'est un mécanisme qui vous permet de répertorier les arguments, du contenu dans le corps d'un trésor et de donner un résultat général de tout cela.
Citation de la proposition: SE-XXXX :


L'idée principale est que nous prenions le résultat de l'expression, y compris les expressions imbriquées comme if et switch, et les formions en un résultat, qui devient la valeur de retour de la fonction actuelle. Cette "construction" est contrôlée par le générateur de fonctions, qui est un attribut personnalisé.

L'original
L'idée de base est que nous prenons les résultats d'expression "ignorés" d'un bloc d'instructions - y compris dans des positions imbriquées comme les corps des instructions de commutation de commutateurs - et les construisons en une seule valeur de résultat qui devient la valeur de retour de la fonction actuelle. La façon dont cette collecte est effectuée est contrôlée par un générateur de fonctions, qui n'est qu'un nouveau type de type d'attribut personnalisé;

Un tel mécanisme vous permet d'écrire du code arborescent déclaratif, sans caractères de ponctuation inutiles:


 let myBody = body { let chapter = spellOutChapter ? "Chapter" : "" div { if useChapterTitles { h1(chapter + "1. Loomings.") } p { "Call me Ishmael. Some years ago" } p { "There is now your insular city" } } } 

Ceci est disponible grâce au nouvel attribut @_functionBuilder . Cet attribut marque un constructeur, il peut s'agir d'une structure. Ce générateur implémente un certain nombre de méthodes spécifiques. De plus, ce générateur est utilisé seul, comme attribut utilisateur dans diverses situations.
Ci-dessous, je montrerai comment cela fonctionne et comment organiser un tel code.


Pourquoi ça?


Apple veut donc prendre en charge le langage DSL intégré spécifique au domaine .
John McCall et Doug Gregor soulignent qu'un tel code est beaucoup plus facile à lire et à écrire - cela simplifie la syntaxe, la rend plus concise et, par conséquent, le code devient plus pris en charge. Dans le même temps, ils notent que leur solution n'est pas une DSL universelle.
Cette solution se concentre sur un ensemble spécifique de problèmes, y compris la description de structures linéaires et arborescentes telles que XML, JSON, hiérarchies de vue, etc.


Comment travailler avec?


Vous pouvez créer votre propre générateur de fonctions, il m'a été plus facile de comprendre comment cela fonctionne. Prenons un exemple primitif d'un générateur qui concatène des chaînes.


 // 1.  Builder @_functionBuilder struct MyBuilder { static func buildBlock(_ atrs: String...) -> String { return atrs.reduce("", + ) } } 

 // 2.          func stringsReduce(@MyBuilder block: () -> String) -> String { return block() } 

 // 3.     let result = stringsReduce { "1" "2" } print(result) // "12" 

Sous le capot, cela fonctionnera comme ceci:


 let result = stringsReduce { return MyBuilder.build("1", "2") } 

Il est important que dans l'implémentation du générateur, les méthodes doivent être exactement statiques, avec des noms spécifiques et avec un type spécifique de paramètres de cette liste . Vous ne pouvez modifier que le type et le nom du paramètre d'entrée.


 static func buildBlock(_ <*atrs*>: <*String*>...) -> <*String*> 

Les noms de méthodes spécifiques seront recherchés dans le générateur et substitués au stade de la compilation. Et si la méthode n'est pas trouvée, une erreur de compilation se produira.
Et c'est magique. Lorsque vous implémentez le générateur, le compilateur ne vous dira rien du tout. Il ne dira pas sur les méthodes disponibles, cela n'aidera pas avec une saisie semi-automatique. Ce n'est que lorsque vous écrivez du code client qui ne peut pas être traité par ce générateur que vous obtenez une erreur indistincte.
Jusqu'à présent, la seule solution que j'ai trouvée est d'être guidée par une liste de méthodes .
Alors pourquoi avons-nous besoin d'autres méthodes? Eh bien, par exemple, pour prendre en charge un tel code avec des contrôles


 stringsReduce { if .random() { //   Bool "one string" } else { "another one" } "fixed string" } 

Pour prendre en charge cette syntaxe, le générateur doit implémenter les buildEither(first:/second:)


 static func buildEither(first: String) -> String { return first } static func buildEither(second: String) -> String { return second } 

Réponse de la communauté


Ce qui est drôle, c'est que ce n'est pas encore dans Swift 5.1, c'est-à-dire qu'une demande de pull avec cette fonctionnalité n'a pas encore été versée, mais néanmoins, Apple l'a déjà ajoutée à Xcode 11 beta. Et dans Function builders → Pitches → Swift Forums, vous pouvez voir la réaction de la communauté à cette proposition.


ViewBuilder


Revenons maintenant à VStack et consultez la documentation de son init initialiseur (alignement: espacement: contenu :) .
Cela ressemble à ceci:


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

Et avant la table de contenu, il y a un attribut personnalisé @ViewBuilder
Il est déclaré comme suit:


 @_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) -> Content where Content : View } 

L'attribut utilisateur est @ _functionBuilder , qui est enregistré au début de sa déclaration.


Et si vous regardez la documentation encore plus bas, vous pouvez voir de nombreuses méthodes statiques buildBlock qui diffèrent par le nombre d'arguments.


Cela signifie que le code d'affichage


 var body: some View { VStack { Text("Placeholder1") Text("Placeholder2") Text("Placeholder3") } } 

sous le capot est converti en tels


  var body: some View { VStack(alignment: .leading) { ViewBuilder.buildBlock(Text("Placeholder1"), Text("Placeholder2"), Text("Placeholder3")) } } 

C'est-à-dire remplit la méthode du générateur buildBlock (:: _ :) .


De la liste entière, la méthode avec le nombre maximum d'arguments est ce type buildBlock (::::::::: :) :) (10 arguments):


 extension ViewBuilder { public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View, C7 : View, C8 : View, C9 : View } 

Et en conséquence, en revenant à l'exemple d'origine, lorsque vous essayez d'augmenter VStack et onze vues à l'intérieur, le compilateur essaie de trouver la méthode ViewBuilder'a buildBlock , qui a 11 arguments d'entrée. Mais il n'y a pas une telle méthode: d'où l'erreur de compilation.
Cela est vrai pour toutes les collections qui utilisent l'initialisation avec l'attribut @ViewBuilder dans l'initialiseur: V | H | Z-Stack, List, Group et autres, à l'intérieur desquelles vous pouvez déclarer plusieurs vues en tant qu'énumération.
Et c'est triste.


MEM (désolé, je n'ai pas trouvé de mème décent)


Comment être


Nous pouvons contourner cette limitation en utilisant ForEach


 struct TestView : View { var body: some View { VStack { ForEach(texts) { i in Text(«\(i)») } } } var texts: [Int] { var result: [Int] = [] for i in 0...150 { result.append(i) } return result } } 

Ou collections imbriquées:


 var body: some View { VStack { VStack { Text("Placeholder_1") Text("Placeholder_2") //   8 } Group { Text("11") Text("12") //   8 } } } 

Mais de telles décisions ressemblent à des béquilles et il n'y a qu'un espoir pour un avenir meilleur. Mais à quoi ressemble cet avenir?
Swift possède déjà des paramètres Variadic . Il s'agit de la capacité d'une méthode à accepter des arguments par énumération. Par exemple, la méthode d' print connue de tous vous permet d'écrire à la fois print(1, 2) et print(1, 2, 3, 4) et ceci sans surcharge inutile de la méthode.


 print(items: Any...) 

Mais cette fonctionnalité du langage n'est pas suffisante, car la méthode buildBlock accepte différents arguments génériques en entrée.
L' ajout de génériques variadiques résoudrait ce problème. Les génériques variadiques vous permettent d'abstraire de nombreux types génériques, par exemple, quelque chose comme ceci:


  static func buildBlock<…Component>(Component...) -> TupleView<(Component...)> where Component: View 

Et Apple est simplement obligé d'ajouter cela. Tout repose maintenant contre ce mécanisme. Et il me semble qu'ils n'ont tout simplement pas réussi à le terminer d'ici la WWDC 2019 (mais ce ne sont que des spéculations).

Source: https://habr.com/ru/post/fr455760/


All Articles