SwiftUI sur les étagères

Chaque fois qu'un nouveau framework apparaît dans un langage de programmation, tôt ou tard, des personnes apparaissent qui en apprennent le langage. C'était probablement le cas dans le développement iOS au moment de l'apparition de Swift: au début, il était considéré comme un ajout à Objective-C - mais je ne l'ai pas déjà trouvé. Maintenant, si vous partez de zéro, le choix de la langue n'en vaut plus la peine. Swift est au-delà de la concurrence.

La même chose, mais à plus petite échelle, se produit avec les cadres. L'apparition de SwiftUI ne fait pas exception. Je suis probablement un représentant de la première génération de développeurs qui a commencé par apprendre SwiftUI, en ignorant UIKit. Cela a un prix - jusqu'à présent, il existe très peu de matériel de formation et d'exemples de code de travail. Oui, le réseau a déjà un certain nombre d'articles racontant une fonctionnalité particulière, un outil particulier. Sur le même site www.hackingwithswift.com, il y a déjà beaucoup d'exemples de code avec des explications. Cependant, ils font peu pour aider ceux qui décident d'apprendre SwiftUI à partir de zéro, comme moi. La plupart des documents du réseau sont des réponses à des questions spécifiques et formulées. Un développeur expérimenté peut facilement comprendre comment tout fonctionne, pourquoi il en est ainsi et pourquoi il doit être appliqué. Pour un débutant, vous devez d'abord comprendre quelle question poser, et alors seulement il pourra accéder à ces articles.



Sous la coupe, je vais essayer de systématiser et trier ce que j'ai réussi à apprendre pour le moment. Le format de l'article est presque un guide, mais plutôt une feuille de triche compilée par moi sous la forme dans laquelle je voudrais moi-même le lire au début de mon voyage. Pour les développeurs expérimentés qui n'ont pas encore plongé dans SwiftUI, il existe également quelques exemples de code intéressants, et les explications textuelles peuvent être lues en diagonale.

J'espère que cet article vous fera gagner du temps lorsque vous voudrez également ressentir un peu de magie.

Pour commencer, un peu de vous


Je n'ai pratiquement aucune expérience en développement mobile et une expérience significative
en 1s ne pouvait pas aider beaucoup ici. A propos de comment et pourquoi j'ai décidé d'apprendre SwiftUI, je vous dirai une autre fois, si cela sera intéressant pour quelqu'un, bien sûr.

Il se trouve que le début de mon immersion dans le développement mobile a coïncidé avec la sortie d'iOS 13 et de SwiftUI. C'est un signe, pensai-je, et j'ai décidé de commencer immédiatement avec, en ignorant UIKit. J'ai trouvé une coïncidence amusante que j'ai commencé à travailler avec 1c à ces moments-là: alors des formulaires gérés sont apparus. Dans le cas de 1c, la vulgarisation des nouvelles technologies a pris près de cinq ans. Chaque fois qu'un développeur était chargé d'implémenter de nouvelles fonctionnalités, il faisait face à un choix: le faire rapidement et de manière fiable, avec des outils familiers, ou passer beaucoup de temps à en discuter avec de nouvelles, et sans garantie de résultats. Le choix était généralement fait en faveur de la vitesse et de la qualité en ce moment, et investir du temps dans de nouveaux outils a été retardé très longtemps.

Maintenant, apparemment, la situation avec SwiftUI est à peu près la même. Tout le monde est intéressé, tout le monde comprend que c'est l'avenir, mais jusqu'à présent, peu ont pris le temps de l'étudier. Sauf pour les projets pour animaux de compagnie.

Dans l’ensemble, peu importe le cadre à étudier, et j’ai décidé de tenter ma chance malgré l’opinion générale qu’il serait possible de le lancer en production dans un an ou deux. Et comme il se trouve que j'étais parmi les pionniers, j'ai décidé de partager mon expérience pratique. Je veux dire que je ne suis pas un gourou, et en général dans le développement mobile - une bouilloire. Néanmoins, j'ai déjà parcouru une certaine voie, au cours de laquelle j'ai cherché sur Internet à la recherche d'informations, et je peux affirmer avec certitude que cela ne suffit pas et que ce n'est pratiquement pas systématisé. Mais en russe, bien sûr, il est pratiquement inexistant. Si c'est le cas, j'ai décidé de rassembler mes forces, de repousser le complexe des imposteurs et de partager avec la communauté ce que j'ai réussi à comprendre moi-même. Je partirai de l'hypothèse que le lecteur connaît déjà au moins très peu SwiftUI, et je ne déchiffrerai pas des choses comme VStack{…} , Text(…) , etc.

J'insiste encore une fois sur le fait que je décrirai plus en détail mes propres impressions des tentatives pour obtenir le résultat souhaité de SwiftUI. Je pourrais très bien ne pas comprendre quelque chose, et à partir de certaines expériences pour tirer des conclusions erronées ou inexactes, toutes les corrections et clarifications sont donc les bienvenues.

Pour les développeurs expérimentés, cet article peut sembler plein de descriptions de choses évidentes, mais ne jugez pas strictement. Les tutoriels pour les nuls sur SwiftUI n'ont pas encore été écrits.

Qu'est-ce que c'est que ce SwiftUI?


Donc, je commencerais probablement par ce dont il s'agit, c'est votre SwiftUI. Là encore, mon 1er passé revient. L'analogie avec les formulaires gérés n'est devenue plus forte que lorsque j'ai regardé quelques vidéos de didacticiel sur la mise en page des interfaces dans Storyboard (c'est-à-dire lorsque vous travaillez avec UIKit). J'ai pris la nostalgie selon des formes «incontrôlables» en 1s: placement manuel des éléments sur le formulaire, et surtout des reliures ... Oh, quand l'auteur de la vidéo de formation pendant environ 20 minutes a parlé des subtilités de la reliure de divers éléments entre eux et des bords de l'écran, je m'en suis souvenu avec un sourire 1C - tout était le même avant les formes contrôlées. Eh bien, presque ... un peu pire, bien sûr, et en conséquence - plus facile. Et SwiftUI est, en gros, des formulaires gérés d'Apple. Aucune reliure. Pas de story-boards et de segways. Vous décrivez simplement la structure de votre vue dans le code. Et c'est tout. Tous les paramètres, tailles, etc. sont définis directement dans le code - mais tout simplement. Plus précisément, vous pouvez modifier les paramètres des objets existants dans Canvas, mais pour cela, vous devez d'abord les ajouter dans le code. Honnêtement, je ne sais pas comment cela fonctionnera dans les grandes équipes de développement, où il est habituel de séparer la mise en page du design et le contenu de la vue elle-même, mais en tant que développeur indépendant, j'aime vraiment cette approche.

Style déclaratif


SwiftUI suppose que la description de la structure de votre vue est entièrement en code. De plus, Apple nous propose un style déclaratif d'écriture de ce code. Autrement dit, quelque chose comme ça:
«Ceci est une vue. Il (pour une raison quelconque, je veux dire «voir» et, par conséquent, appliquer la déclinaison à un mot féminin) se compose de deux champs de texte et d'une image. Les champs de texte sont disposés les uns après les autres horizontalement. L'image est sous eux et ses bords sont rognés en forme de cercle. "
Cela semble inhabituel, non? Habituellement, dans le code, nous décrivons le processus lui-même, ce qui doit être fait pour atteindre le résultat que nous avons dans notre tête:
"Insérez un bloc, insérez un champ de texte dans ce bloc, suivi d'un autre champ de texte, puis prenez une photo, recadrez ses bords en les arrondissant et collez ci-dessous."
Cela ressemble à une instruction pour les meubles d'Ikea. Et dans swiftUI, nous voyons immédiatement quel devrait être le résultat. Même sans Canvas ni débogage, la structure du code reflète clairement la structure de la vue. Il est clair quoi et dans quelle séquence sera affiché et avec quels effets.

Un excellent article sur FunctionBuilder et comment il vous permet d'écrire du code dans un style déclaratif est déjà sur Habré .

En principe, beaucoup de choses ont été écrites sur le style déclaratif et ses avantages, je vais donc terminer. J'ajouterai de moi-même que je m'y suis un peu habitué et j'ai vraiment senti à quel point il est pratique d'écrire du code dans ce style en ce qui concerne les interfaces. Avec cela, Apple a frappé ce qu'on appelle la bulle!

En quoi consiste View?


Mais regardons de plus près. Apple suggère que le style déclaratif est le suivant:

 struct ContentView: View { var text1 = "some text" var text2 = "some more text" var body: some View { VStack{ Text(text1) .padding() .frame(width: 100, height: 50) Text(text2) .background(Color.gray) .border(Color.green) } } } 

Veuillez noter que View est une structure avec quelques paramètres. Pour créer la View la structure, nous devons définir le body paramètre calculé, qui renvoie une some View . Nous en reparlerons plus tard. Le contenu du body: some View { … } fermeture de la body: some View { … } est la description de ce qui sera affiché à l'écran. En fait, c'est tout ce qui est nécessaire pour que notre structure réponde aux exigences du protocole View. Je suggère de me concentrer principalement sur le body .

Et donc, des étagères


Au total, j'ai compté trois types d'éléments à partir desquels le corps de la vue est construit:

  • Autre vue
    C'est-à-dire Chaque vue contient une ou plusieurs autres View . Ceux-ci, à leur tour, peuvent également contenir à la fois une View système prédéfinie comme Text() et des images complexes personnalisées écrites par le développeur. Il s'avère une sorte de poupée gigogne avec un niveau de nidification illimité.
  • Modificateurs
    Avec l'aide de modificateurs, toute magie opère. Grâce à eux, nous disons brièvement et clairement à SwiftUI quel type de vue nous voulons voir. Comment cela fonctionne, nous allons toujours le comprendre, mais l'essentiel est que les modificateurs ajoutent la pièce requise au contenu d'une certaine View .
  • Conteneurs
    Les premiers conteneurs avec HStack commence le standard «Hello, world» sont HStack et VStack . Un peu plus tard, Group , Section et d'autres apparaissent. En fait, les conteneurs sont la même vue, mais ils ont une fonctionnalité. Vous leur passez du contenu que vous souhaitez afficher. Toute la caractéristique du conteneur est qu'il doit en quelque sorte regrouper et afficher les éléments de ce contenu. En ce sens, les conteneurs sont similaires aux modificateurs, à la seule différence que les modificateurs sont destinés à modifier une vue prête à l'emploi, et les conteneurs organisent ces vues (éléments de contenu ou blocs de syntaxe déclarative) dans un certain ordre, par exemple, verticalement ou horizontalement ( VStack{...} HStack{...} ). Il existe également des conteneurs spécifiques, tels que ForEach ou GeometryReader , nous en parlerons un peu plus tard.

    En général, je considère les conteneurs comme n'importe quelle vue, dans laquelle le contenu peut être transmis en tant que paramètre.

Et c'est tout. Tous les éléments de SwiftUI de race pure peuvent être attribués à l'un de ces types. Oui, cela ne suffit pas pour remplir votre vue de fonctionnalités, mais c'est tout ce dont vous avez besoin pour afficher vos fonctionnalités à l'écran.

.modifiers () - comment sont-ils organisés?


Commençons par le plus simple. Un modificateur est en fait une chose très simple. Il prend simplement une View , lui applique des modifications (ou le fait-il?) , Et la renvoie. C'est-à-dire Le modificateur est une fonction de la View elle-même, qui renvoie self , après avoir effectué quelques modifications auparavant.

Voici un exemple de code avec lequel je déclare mon propre modificateur. Plus précisément, je surcharge le frame(width:height:) modification existant frame(width:height:) , avec lequel vous pouvez fixer les dimensions spécifiques d'une vue particulière. Dans la case correspondante, vous devez spécifier la largeur et la hauteur, et je devais lui passer un objet CGSize avec un argument, qui est une description de la longueur et de la largeur. Pourquoi ai-je besoin de ça, je le dirai un peu plus tard.

 struct FrameFromSize: ViewModifier{ let size: CGSize func body(content: Content) -> some View { content .frame(width: size.width, height: size.height) } } 

Avec ce code, nous avons créé une structure conforme au protocole ViewModifier . Ce protocole nous oblige à ce que la fonction body() soit implémentée dans cette structure, dont l'entrée sera du Content et la sortie aura some View : le même type que le paramètre body de notre View (nous parlerons d'une vue ci-dessous) . De quel type de Content il?

Content + ViewBuilder = Afficher


Dans la documentation intégrée à son sujet, il est dit:
`content` est un proxy pour la vue à laquelle le modificateur représenté par` Self` lui sera appliqué.
Il s'agit d'un type de proxy, qui est un préfabriqué View auquel des modificateurs peuvent être appliqués. Une sorte de produit semi-fini. En fait, le Content est une fermeture de style déclarative qui décrit la structure de la View . Ainsi, si nous appelons ce modificateur pour une vue, tout ce qu'il fait est d'obtenir une fermeture du body et de la transmettre à notre fonction body , dans laquelle nous ajoutons nos cinq cents à cette fermeture.

Encore une fois, View est avant tout une structure qui stocke tous les paramètres nécessaires pour générer une image à l'écran. Y compris les instructions de montage, dont le Content . Ainsi, une fermeture dans un style déclaratif ( Content ) traité à l'aide de ViewBuilder nous renvoie une Vue.

Revenons à notre modificateur. En principe, la déclaration de la structure FrameFromSize suffit déjà pour commencer à l'appliquer. À l'intérieur du body nous pouvons écrire comme ceci:

 RoundedRectangle(cornerRadius: 4).modifier(FrameFromSize(size: size)) 

modifier est une méthode de protocole View qui extrait le contenu de la View modification, le transmet à la fonction corps de la structure du modificateur et transmet le résultat au traitement ViewBuilder ou au modificateur suivant, si nous avons une chaîne de modifications.

Mais vous pouvez le rendre encore plus concis en déclarant votre propre modificateur en tant que fonction, élargissant ainsi les capacités du protocole View.

 extension View{ func frame(_ size: CGSize) -> some View { self.modifier(FrameFromSize(size: size)) } } 

Dans ce cas, j'ai surchargé le modificateur existant .frame(width: height:) une autre variante des paramètres d' .frame(width: height:) . Maintenant, nous pouvons utiliser l'option d'appeler le modificateur frame(size:) pour n'importe quelle View . Il s'est avéré que rien de compliqué.

Un peu d'erreurs
Au fait, je pensais qu'il n'était pas nécessaire d'étendre tout le protocole, il suffirait d'étendre spécifiquement le RoundedRectangle dans mon cas, et cela aurait dû fonctionner, comme il me semblait - mais il semble que Xcode ne s'attendait pas à une telle impudence, et est tombé avec une erreur inintelligible " Abort trap: 6 ”et une proposition pour envoyer un vidage aux développeurs. D'une manière générale, dans SwiftUI, les descriptions d'erreurs jusqu'à présent ne révèlent pas très souvent la cause de cette erreur.

De la même manière, vous pouvez créer des modificateurs personnalisés et les utiliser de la même manière que le SwiftUI intégré:

 RoundedRectangle(cornerRadius: 4).frame(size) 

De façon pratique, concise et claire.

J'imagine une chaîne de modifications comme des perles enfilées sur un fil - notre point de vue. Cette analogie est également vraie dans le sens où l'ordre dans lequel les modifications sont appelées importe.



Presque tout dans SwiftUI est View
Au fait, une remarque intéressante. En tant que paramètre d'entrée, l'arrière-plan n'accepte pas la couleur, mais la vue. C'est-à-dire La classe Color n'est pas seulement une description de la couleur, c'est une vue à part entière, à laquelle des modificateurs et plus peuvent être appliqués. Et comme arrière-plan, en conséquence, vous pouvez passer une autre vue.

Modificateurs - uniquement pour les modifications
Peut-être vaut-il la peine de noter un dernier point. Les modificateurs qui ne modifient pas le contenu source sont simplement ignorés par SwiftUI et ne sont pas appelés. C'est-à-dire Vous ne pourrez pas effectuer de déclenchement basé sur le modificateur qui provoque certains événements, mais n'effectue aucune action avec le contenu. Apple nous pousse constamment à abandonner certaines actions lors de l'exécution lors du rendu de l'interface et à faire confiance au style déclaratif.

Voir toujours


Plus tôt, nous avons parlé de la composition du body , du corps de la View ou de ses instructions de montage. Revenons à la View elle-même. Tout d'abord, c'est une structure dans laquelle certains paramètres peuvent être déclarés, et body n'est que l'un d'entre eux. Comme nous l'avons déjà dit, en découvrant ce qu'est le Content , le body est une instruction sur la façon d'assembler la vue souhaitée, qui est une fermeture dans un style déclaratif. Mais qu'est-ce qui devrait rendre notre fermeture?

quelques vues - commodité




Et nous arrivons en douceur à une question que pendant longtemps je n'ai pas pu comprendre, même si cela ne m'a pas empêché d'écrire du code de travail. Quelle est cette some View ? La documentation indique que cette description est un "type de résultat opaque" - mais cela n'a pas beaucoup de sens.

Le mot clé some est une version «générique» d'une description d'un type retourné par une fermeture qui ne dépend pas d'autre chose que du code lui-même. C'est-à-dire Le résultat de l'accès à la propriété calculée du corps de notre vue devrait être une structure qui satisfait le protocole de vue. Il peut y en avoir beaucoup - texte, image ou peut-être une structure que vous avez déclarée. La puce entière du mot clé some est de déclarer «générique» conforme au protocole View. Il est statiquement déterminé par le code implémenté dans le corps de votre vue, et Xcode est tout à fait capable d'analyser ce code et de calculer la signature spécifique de la valeur de retour (enfin, presque toujours) . Et certains sont juste une tentative de ne pas surcharger le développeur de cérémonies inutiles. Il suffit que le développeur dise: "il y aura une sorte de vue", et laquelle - triez-la vous-même. La clé ici est que le type concret est déterminé non pas par les paramètres d'entrée, comme avec le type générique habituel, mais directement par le code. Par conséquent, ci-dessus, générique que j'ai cité.

Xcode devrait être en mesure d'identifier un type spécifique sans savoir exactement quelles valeurs vous passez à cette structure. Ceci est important à comprendre - après la compilation, l'expression Some View est remplacée par le type spécifique de votre View . Ce type est assez déterministe et peut être assez complexe, par exemple, comme ceci: Group<TupleView<(Text, ForEach<[SomeClass], SomeClass.ID, Text>)>> .

Un exemple de code peut être restauré à partir de ce type:

 Group{ Text(…) ForEach(…){(value: SomeClass) in Text(…) } } 

ForEach , comme le montre la signature de type, n'est pas une boucle d'exécution. Ceci est juste une View qui est construite sur la base d'un tableau d'objets SomeClass . comme identifiant d'une sous-vue particulière associée à l'élément de collection, l'ID d'élément est indiqué, et pour chaque élément une sous- subView type Text est subView . Text et ForEach combinés dans TupleView , et tout cela est placé dans Group . Nous parlerons plus ForEach détail de ForEach .

Imaginez le volume d'écriture si nous étions obligés de décrire une signature exacte comme le body du paramètre? Pour éviter cela, le mot clé some été créé.

Résumé
some sont «génériques - vice versa». Nous obtenons le générique classique de l'extérieur de la fonction, et connaissant déjà le type spécifique du type générique, Xcode définit le fonctionnement de notre fonction. some- ne dépendent pas des paramètres d'entrée, mais uniquement du code lui-même. Il s'agit simplement d'une abréviation, qui permet de ne pas définir de type spécifique, mais d'indiquer uniquement la famille de la valeur retournée par la fonction (protocole).

quelques vues - et conséquences


L'approche du calcul du type statique d'une expression à l'intérieur du corps soulève, à mon avis, deux points importants:

  • Une fois compilé, Xcode analyse le contenu du corps afin de calculer le type de retour spécifique. Dans les corps complexes, cela peut prendre un certain temps. Dans certains corps particulièrement complexes, il peut ne pas être en mesure de faire face à un temps sain du tout, et il le dira directement.

    En général, View doit être aussi simple que possible. Les structures complexes sont mieux placées dans une vue séparée. Ainsi, des chaînes entières de types réels sont remplacées par un type - votre CustomView, qui permet au compilateur de ne pas devenir fou avec tout ce gâchis.
    Soit dit en passant, il est vraiment très pratique de déboguer un petit morceau d'une grande vue, ici, à la volée, en recevant et en observant le résultat dans Canvas.
  • Nous ne pouvons pas contrôler directement le flux. Si If - else SwiftUI peut toujours le traiter en créant une «vue Schrödinger» de type <_ConditionalContent <Text, TextField >>, alors l'opérateur de condition trinar ne peut être utilisé que pour sélectionner une valeur de paramètre spécifique, mais pas un type, ni même pour sélectionner une séquence de modificateurs.

    Mais cela vaut la peine de restaurer le même ordre de modificateurs, et un tel enregistrement cesse d'être un problème.

Sauf corps


Cependant, il peut y avoir d'autres paramètres dans la structure avec lesquels vous pouvez travailler. En tant que paramètres, nous pouvons déclarer les choses suivantes.

Paramètres externes


Ce sont des paramètres de structure simples que nous devons passer de l'extérieur lors de l'initialisation pour que la vue les rende en quelque sorte:

 struct TextView: View { let textValue: String var body: some View { Text(textValue) } } 

Dans cet exemple, textValue pour la structure TextView est un paramètre qui doit être TextView externe car il n'a pas de valeur par défaut. Étant donné que les structures prennent en charge la génération automatique d'initialiseurs, nous pouvons utiliser cette vue simplement:

  TextView(textValue: "some text") 

De l'extérieur, vous pouvez également transférer les fermetures qui doivent être effectuées lorsqu'un événement se produit. Par exemple, Button(lable:action:) ne fait que cela: effectue la fermeture de l'action passée lorsqu'un bouton est cliqué.

état - paramètres


SwiftUI utilise très activement la nouvelle fonctionnalité Swift 5.1 - Property Wrapper .

Tout d'abord, ce sont des variables d'état - des paramètres stockés de notre structure, dont le changement devrait se refléter sur l'écran.Ils sont emballés dans des emballages spéciaux @State- pour les types primitifs et @ObservedObject- pour les classes. Une classe doit satisfaire au protocole ObservableObject- cela signifie que cette classe doit être en mesure d'informer les abonnés (View, qui utilisent cette valeur avec un wrapper @ObservedObject) de la modification de leurs propriétés. Pour ce faire, enveloppez simplement les propriétés requises dans @Published.

Si vous ne cherchez pas de moyens simples ou si vous avez besoin de fonctionnalités supplémentaires, au lieu de ce wrapper, vous pouvez utiliser ObservableObjectPublisheret envoyer des notifications manuellement en utilisant les événements de willSet()ces paramètres, comme décrit, par exemple, ici .

Rappelez-vous, j'ai dit quebodyS'agit-il simplement d'une propriété calculable? Au début, je ne comprenais pas tout de suite les variables d'état, et j'ai essayé de déclarer quelques variables d'état à l'intérieur bodysans wrappers. Le problème s'est avéré être body, comme je l'ai dit, une instruction apatride. La vue a été générée conformément à cette instruction, et tout le contexte déclaré à l'intérieur du corps a été mis en décharge. Ensuite, seuls les paramètres de structure stockés sont actifs. Lors de la modification des paramètres d'état, tous les nôtres sont Viewmis à jour. L'instruction est à nouveau prise, les valeurs actuelles de tous les paramètres de structure y sont substituées, l'image sur l'écran est collectée, l'instruction est renvoyée jusqu'à la prochaine fois. Variables déclarées à l'intérieur body- avec elle. Pour les développeurs expérimentés, cela peut être évident, mais au début, j'étais tourmenté par cela, ne comprenant pas l'essence du processus.

Et encore une remarque
Vous ne pouvez pas utiliser d' didSet willSetévénements de paramètres de structure encapsulés dans des wrappers. Le compilateur vous permet d'écrire ce code, mais il ne s'exécute pas. Probablement parce que l'encapsuleur est une sorte de code de modèle qui est exécuté lorsque ces événements se produisent.

Exemple d' état classique :

 struct ContentView: View { @State var tapCount = 0 var body: some View { VStack { Button(action: {self.tapCount += 1}, label: { Text("Tap count \(tapCount)") }) } } } 

Paramètres de liaison


Eh bien, pour refléter certains changements dans le service d'affichage @State @ObservedObject. Mais comment ces changements se transmettent-ils entre les deux View? Pour ce faire, SwiftUI a un autre PropertyWrapper - @Binding. Compliquons notre exemple avec un bouton pour compter les clics. Supposons que nous ayons un parent View, qui reflète, entre autres, un compteur de clics et un enfant Viewavec un bouton. Dans la vue parent, le compteur est déclaré comme @State- c'est compréhensible, mais nous voulons que le compteur à l'écran soit mis à jour. Mais dans la filiale, le compteur doit être déclaré comme @Binding. Il s'agit d'un autre Property Wrapper, à l'aide duquel nous déclarons des paramètres de structure qui non seulement changeront, mais reviendront également au parent View. Ceci est une sorte de inoutmarqueur pourView. Lorsqu'une valeur change dans une vue enfant, cette modification est traduite dans la vue parent, d'où elle est originaire. Et comme pour inout, nous devons marquer les valeurs transmises avec un symbole spécial $,pour montrer que nous attendons que la valeur transmise change dans une autre vue. Réagissez en action.

 struct ContentView: View { @State var tapCount = 0 var body: some View { VStack{ SomeView(count: $tapCount) Text("you tap \(tapCount) times") } } } 

Cela se reflète également dans les types de données. @Binding var tapCount: Intpar exemple, ce n'est plus seulement un Inttype, c'est

 Binding<Int> 

C'est utile de savoir, par exemple, si vous voulez écrire votre propre initialiseur View.

 struct SomeView: View{ @Binding var tapCount: Int init(count: Binding<Int>){ self._tapCount = count //    -    } var body: some View{ Button(action: {self.tapCount += 1}, label: { Text("Tap me") }) } } 

Veuillez noter qu'à l'intérieur, initvous devez @PropertyWrapperutiliser un trait de soulignement pour faire référence aux paramètres enveloppés dans certains paramètres self._- cela fonctionne dans les initialiseurs, lorsqu'ils sont selfencore en cours de création. Plus précisément, avec l'aide, self._nous nous référons au paramètre avec son wrapper. L'application directe à la valeur à l'intérieur du wrapper se fait sans soulignement.

À son tour, si vous avez une variable enveloppée dans une sorte d'entrée PropertyWrapper, nous obtenons un type wrapper, dans ce cas

 Binding<Int> 

Vous Intpouvez accéder directement à la valeur de type .wrappedValue.

Et comme toujours, un râteau personnel
, Binding . View View. View , @Binding-. , View State @Binding — , State- Binding. -, , .

EnvironmentObject


En bref, les EnvironmentObjectparamètres sont comme Binding, seulement immédiatement pour tout Viewle monde dans la hiérarchie, sans avoir besoin de les passer explicitement.

 ContentView().environmentObject(session) 

Généralement, l'état actuel de l'application, ou une partie de celle-ci, dont de nombreux View ont besoin à la fois, est transmis. Par exemple, des données sur un utilisateur, une session ou quelque chose de similaire, il est logique de les placer une fois dans EnvironmentObject, dans la vue racine. Dans chaque vue, là où elles sont nécessaires, elles peuvent être extraites de l'environnement en déclarant une variable avec un wrapper @EnvironmentObject, par exemple comme ceci

  @EnvironmentObject var session: Session 

L'identifiant d'une valeur spécifique est le type lui-même. Si vous entrez EnvironmentObjectplusieurs valeurs du même type, l'ordre est important. Pour arriver à la 3e, par exemple, la valeur, vous devez obtenir toutes les valeurs dans l'ordre, même si vous n'en avez pas besoin. Par conséquent, EnvironmentObjectil est bien adapté pour refléter l'état de l'application, mais il n'est pas bien adapté pour passer plusieurs valeurs du même type entre View. Ils devront être transmis manuellement, via Binding.

@Environment est presque le même. Dans le sens, c'est un état de l'environnement, c'est-à-dire OS Il est pratique de passer à travers ce wrapper, par exemple, la position de l'écran (vertical ou horizontal), un thème clair ou sombre est utilisé, etc. De plus, grâce à ce wrapper, vous pouvez accéder à la base de données lorsque vous utilisez CoreData:

 @Environment(\.managedObjectContext) var moc: NSManagedObjectContext 

Soit dit en passant, beaucoup de choses intéressantes ont été faites pour travailler avec CoreData dans SwiftUI. Mais à ce sujet, peut-être, la prochaine fois. L'article a donc dépassé toutes les attentes.

@PropertyWrapper personnalisé


En gros, PropertyWrapperc'est un raccourci pour setter et getter, le même pour tous les paramètres enveloppés dans le même property wrapper. Vous pouvez restaurer complètement cette fonctionnalité vous-même en supprimant la déclaration wrapper et en écrivant les paramètres getter {} setter {}, mais vous devrez le faire à chaque fois, pour chacun View, en dupliquant le code. Par exemple, son utilisation est PropertyWrappertrès pratique pour masquer le travail UserDefaults.

 @propertyWrapper struct UserDefault<T> { var key: String var initialValue: T var wrappedValue: T { set { UserDefaults.standard.set(newValue, forKey: key) } get { UserDefaults.standard.object(forKey: key) as? T ?? initialValue } } } 

Ainsi, nous pouvons stocker des types de données primitifs dans le stockage UserDefaults. Apple revendique une très bonne vitesse d'accès à ce stockage, il n'est donc probablement pas nécessaire de mettre ces données en cache sous forme de paramètres de structure ou de variables, à moins bien sûr qu'elles ne soient utilisées dans des cycles massifs et des tâches exigeantes en vitesse.

Compte tenu de cela, vous pouvez créer un type de stub (dans ce cas, une énumération) dans lequel déclarer des variables statiques pour accéder à des valeurs spécifiques stockées à l' UserDefaultsaide de l'encapsuleur qui vient d'être créé:

 enum UserPreferences { @UserDefault(key: "isCheatModeEnabled", initialValue: false) static var isCheatModeEnabled: Bool @UserDefault(key: "highestScore", initialValue: 10000) static var highestScore: Int @UserDefault(key: "nickname", initialValue: "cloudstrife97") static var nickname: String } 

Le résultat peut être utilisé très succinctement, en se concentrant sur la logique et l'affichage visuel, et tout le travail est effectué sous le capot.

 UserPreferences.isCheatModeEnabled = true UserPreferences.highestScore = 25000 UserPreferences.nickname = "squallleonhart” 

Un exemple a été initialement décrit ici .

Conteneurs


Eh bien, la dernière chose à discuter de ma liste est les conteneurs. Nous en avons déjà partiellement parlé lorsque nous en avons parlé body. En fait, les conteneurs sont courants View. La seule différence est qu'en tant que l'un des paramètres de cette structure, nous transmettons le contenu. Rappelons que le contenu est une fermeture contenant une ou plusieurs expressions déclaratives. Cette fermeture, si elle est traitée à l'aide de @ViewBuilder, nous renverra une nouvelle vue, combinant d'une certaine manière toutes les vues répertoriées dans la fermeture (blocs de contenu). Dans le même temps, pour différents conteneurs, les mécanismes de traitement des blocs eux-mêmes sont différents. VStackorganise les éléments de contenu verticalement, HStackhorizontalement, etc. C'est comme un modificateur, mais cette fois ce n'est pas seulement une vue spécifique qui est en train d'être modifiée, mais l'ensembleContenttransféré dans le conteneur et un nouveau est généré View. De plus, ce nouveau Viewa un nouveau type. Par exemple, pour HStack{Text(…)}ce type sera TupleView<Text, Image>.

Cependant, n'oubliez pas que tout View, y compris les conteneurs, est une structure qui peut avoir d'autres paramètres que le corps. Par exemple, pendant longtemps, je n'ai pas pu comprendre comment supprimer un petit espace entre l' Text(«a») Text(«b»)intérieur HStack. J'ai passé beaucoup de temps avec offset()et position(), calculant les coordonnées du décalage en fonction de la longueur des lignes, jusqu'à ce que je tombe par hasard sur la syntaxe complète de la déclaration HStack:
HStack (espacement:, alingment:, contexte :).
Simplement, les deux premiers paramètres sont facultatifs et sont ignorés dans la plupart des exemples. Erreur pour les débutants - ne pas voir la syntaxe complète.

Foreach


Séparément, il vaut la peine d'en parler ForEach. Il s'agit d'un conteneur qui sert à refléter à l'écran tous les éléments de la collection transférée. Tout d'abord, vous devez comprendre que ce n'est pas la même chose que d'appeler une collection forEach(…). Comme nous l'avons dit ci-dessus, ForEachretourne un unique View, créé sur la base des éléments de la collection transférée. C'est-à-direc'est juste un autre conteneur dans lequel la collection est transférée et des instructions sur la façon de refléter les éléments de la collection à l'écran.
De plus, il ForEachdoit être placé à l'intérieur d'un autre conteneur qui déterminera déjà comment grouper cette entité multiple - en la plaçant verticalement, horizontalement ou, par exemple, en la plaçant dans une liste ( List).

ForEachaccepte trois paramètres: collection ( data: RandomAccesCollection), adresse de l'identificateur d'élément de collection ( id: Hashable) et content ( content: ()->Content). Le troisième dont nous avons déjà discuté: comme tout autre conteneur, il ForEachaccepte Content- c'est-à-dire court-circuit. Mais contrairement aux conteneurs ordinaires, où il contentne contient pas de paramètres, il ForEachpasse à la fermeture un élément de collection qui peut être utilisé pour décrire le contenu.

La collection ForEachne convient à personne , mais seulement RandomAccesCollection. Pour diverses collections désordonnées, il suffit d'appeler la méthode sorted(by:)avec laquelle vous pouvez obtenir RandomAccesCollection.

ForEach- Il s'agit d'un ensemble subViewgénéré pour chaque élément de la collection en fonction du contenu transmis. Il est important de noter que SwiftUI doit savoir lequel subViewest associé à quel élément de la collection. Pour cela, chacun Viewdoit avoir un identifiant. Le deuxième paramètre est précisément nécessaire pour cela. Si les éléments de la collection sont des Hashabletypes, tels que des chaînes, vous pouvez écrire simplement id: \.self. Cela signifie que la chaîne elle-même sera l'identifiant. Si les éléments de la collection sont des classes et satisfont au protocoleIdentifiable- alors le deuxième argument peut être manqué. Dans ce cas, l'id de chaque élément de la collection deviendra un identifiant subView. Si votre objet a une sorte d'accessoires qui fournissent l'unicité et qui répondent au protocole Hashable, vous pouvez le spécifier comme ceci:

 ForEach(values, id: \.value){item in …} 

Dans mon exemple, valuesest un tableau d'objets de classe SomeObjectpour lesquels les accessoires sont déclarés value: Int. Dans tous les cas, vous devez vous assurer que chaque identifiant Viewassocié à un élément de votre collection est unique . Par exemple, dans votre contexte, certains paramètres de votre objet peuvent changer. Viewil doit être mis en correspondance 1 à 1 avec l'objet de données (élément de collection), sinon il ne sera pas clair où renvoyer la modification de @Bindingparamètre View.

Soit dit en passant, l'organisation d'une analyse des éléments de collection qui ne satisfont pas Identifiable peut également être effectuée à l'aide d'index. Par exemple, comme ceci:
 ForEach(keys.indices){ind in SomeView(key: self.keys[ind]) } 

Dans ce cas, la traversée sera construite non pas par les éléments eux-mêmes, mais par leurs indices. Pour les petites collections, cela est parfaitement acceptable. Sur les collections avec un grand nombre d'éléments, cela peut probablement affecter les performances, en particulier lorsque les éléments de la collection ne sont pas des types de référence, mais, par exemple, des chaînes volumineuses ou des données JSON. En général, utilisez avec prudence.

Un point important sur Contentce à quoi on fait référence ForEach. Il est de mauvaise humeur et refuse de travailler normalement avec des fermetures, avec plus d'un bloc (c'est-à-dire qu'il perçoit le contenu d'une ligne normalement, mais il n'en a déjà pas 2 ou plus). Cela est résolu assez simplement, il est assez simple de remplir tout le contenu Groupe{}- un tel hack cesse d'être un problème.

Il suffit de déclarer que les variables internes dans le cadre de cette fermeture ne fonctionneront pas. Les fermetures transmises à ViewBuilder ne peuvent pas contenir de déclarations de variables. Rappelez-vous, au début de l'article, j'ai donné un exemple de création d'un modificateur .frame(size:)? Je l'ai créé pour cette raison. J'ai calculé les tailles de bouton en fonction du nombre de ces boutons dans une rangée et du nombre de lignes (je n'étais pas satisfait de l'étirement automatique, différents boutons devraient avoir des tailles différentes). La fonction a renvoyé CGSize et plusieurs niveaux de structures imbriquées ont été explorés à l'intérieur. S'il était possible d'exécuter la fonction une fois, écrivez son résultat sous la forme d'une taille variable, puis appelez.ftame(width: size.width, height: size.height)"Je ferais ça." Mais il n'y a pas une telle possibilité, et je ne voulais pas exécuter la fonction deux fois - parce que j'ai contourné cette limitation et mis une partie du code dans le modificateur.

Vue conteneur personnalisée


Eh bien, comme c'est arrivé, je vais donner un exemple de création d'un conteneur personnalisé. Assez souvent, la relation de plusieurs objets de type «1: N» peut être représentée de manière pratique sous la forme d'un dictionnaire. Il n'est dict: [KeyObject: [SomeObject]]pas difficile d' exécuter la requête et de convertir son résultat en un dictionnaire de types .

Dans ce cas, les objets de la classe agissent comme la clé du dictionnaire KeyObject(pour cela, il doit supporter le protocole Hashable), et les valeurs sont des tableaux d'objets d'une autre classe - SomeObject.

 class SomeObject: Identifiable{ let value: Int public let id: UUID = UUID() init(value: Int){ self.value = value } } class KeyObject: Hashable, Comparable{ var name: String init(name: String){ self.name = name } static func < (lhs: KeyObject, rhs: KeyObject) -> Bool { lhs.name < rhs.name } static func == (lhs: KeyObject, rhs: KeyObject) -> Bool { return lhs.name == rhs.name } func hash(into hasher: inout Hasher) { hasher.combine(name) } } 

Si votre application prévoit une sorte d'analyse avec le regroupement, il est logique de créer un conteneur séparé pour afficher ces dictionnaires afin de ne pas dupliquer tout le code dans chaque vue. Et étant donné que les regroupements peuvent être modifiés par l'utilisateur, nous devrons utiliser des génériques. Je ne l'ai pas compliqué en ajoutant du design visuel, ne laissant que la structure de notre container:

 struct TreeView<K: Hashable, V: Identifiable, KeyContent, ValueContent>: View where K: Comparable, KeyContent: View, ValueContent: View{ let data: [K: [V]] let keyContent: (K)->KeyContent let valueContent: (V)->ValueContent var body: some View{ VStack(alignment: .leading, spacing: 0){ ForEach(data.keys.sorted(), id: \.self){(key: K) in VStack(alignment: .trailing, spacing: 0){ self.keyContent(key) ForEach(self.data[key]!){(value: V) in self.valueContent(value) } } } } } } 

Comme vous pouvez le voir, le conteneur accepte un dictionnaire du type [K: [V]](où Kest le type des objets de clé de dictionnaire, Vest le type de tableau dans lequel les valeurs de dictionnaire se composent), et deux contextes: l'un pour afficher les clés de dictionnaire, et l'autre pour afficher les valeurs. Malheureusement, je n'ai pas trouvé d'exemples de création ViewBuilder-de conteneurs personnalisés pour les conteneurs personnalisés (probablement cette option n'existe tout simplement pas), nous devrons donc utiliser celui standard ForEach. Puisqu'il n'accepte qu'à l'entrée RandomAccessCollection, et dict.keysce n'est pas le cas, nous devrons utiliser le tri. D'où la nécessité de prendre en charge le protocole Comparablek KeyObject.

J'ai utilisé deux ForEachconteneurs imbriqués . Dans le premier cas, j'ai utilisé le hachage de l'élément de collection (\.self) comme identifiant de chaque imbriqué View. Je pourrais faire ça parce que les clés de dictionnaire doivent de toute façon prendre en charge le protocole Hashable. Dans le deuxième cas, j'ai ajouté le SomeObjectsupport de protocole à la classeIdentifiable. Cela m'a permis de ne pas spécifier du tout de clé de communication - id est automatiquement utilisé. Dans mon cas, l'identifiant n'est stocké nulle part. Chaque fois qu'un objet est créé - qu'il s'agisse de création dans du code ou de récupération à l'aide d'une requête de base de données - un nouvel identifiant est généré. Pour une interface, ce n'est pas indispensable. Il ne changera pas tout au long de la vie de l'objet, c'est-à-dire session, et cela suffit pour l'afficher sous cet identifiant. Et si la prochaine fois que vous ouvrirez l'application, elle aura un identifiant différent - rien de mauvais ne se produira. Si votre objet possède déjà des champs clés, vous pouvez simplement faire de id un paramètre calculé et utiliser de toute façon le support de ce protocole et une syntaxe abrégée ForEach.

Un exemple d'utilisation de notre conteneur:

 struct ContentView: View { let dict: [KeyObject: [SomeObject]] = [ KeyObject(name: "1st group") : [SomeObject(value: 1), SomeObject(value: 2), SomeObject(value: 3)], KeyObject(name: "2nd group") : [SomeObject(value: 4), SomeObject(value: 5), SomeObject(value: 6)], KeyObject(name: "3rd group") : [SomeObject(value: 7), SomeObject(value: 8), SomeObject(value: 9)] ] var body: some View { TreeView(data: dict, keyContent: {keyObject in Text("the key is: \(keyObject.name)") } ){valueObject in Text("value: \(valueObject.value)") } } } 

et le résultat à l'écran dans Canvas:



à suivre


C'est tout pour l'instant. Je voulais également souligner tous les râteaux sur lesquels j'ai marché, en essayant de l'utiliser CoreDataen conjonction avec SwiftUI, mais, franchement, je ne m'attendais pas à ce que seules les bases de SwiftUI prennent autant de temps et que l'article soit si volumineux. Donc, comme on dit, à suivre.

Si vous avez quelque chose à ajouter ou à corriger, bienvenue dans les commentaires. Je vais essayer de refléter des commentaires importants dans l'article.

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


All Articles