Communitygithub.com

gtokman/skills

Comment SwiftUI décide ce qui doit être réaffiché — identité, durée de vie, dépendances — et comment écrire des vues qui se mettent à jour efficacement, en particulier SwiftUI hébergé dans des cellules UIKit (UIHostingConfiguration) et les vues reconstruites à partir de contenu diffusé ou généré. À utiliser lors de l'écriture ou de la révision de vues SwiftUI pour les performances, la correction de vues qui perdent leur état, clignotent, se réaffichent trop souvent ou rechargent des images, le choix des identifiants ForEach, l'hébergement de SwiftUI dans des cellules de collection/tableau, ou le débogage de mises en page qui changent lorsque les vues sont encapsulées. Basé sur les sessions WWDC21 « Demystify SwiftUI » et WWDC23 « Demystify SwiftUI performance », ainsi que sur des leçons vérifiées en pratique.

Qu'est-ce que skills ?

Cette compétence fournit des directives détaillées sur le modèle de rendu de SwiftUI, en se concentrant sur l'identité, la durée de vie et les dépendances. Elle explique comment SwiftUI détermine ce qu'il faut mettre à jour et comment éviter les problèmes courants tels que les réaffichages inutiles, la perte d'état, les animations incorrectes ou les saccades. La compétence offre des règles concrètes pour gérer l'identité des vues (via des identifiants stables et uniques, et en évitant les identifiants basés sur l'index), pour optimiser les dépendances (en passant uniquement les données nécessaires aux vues, en utilisant `@Observable` correctement, et en évitant les fermetures non comparables dans l'environnement), et pour maintenir un `body` peu coûteux (en évitant le travail lourd dans le corps de la vue). Elle traite également de l'utilisation de SwiftUI dans les cellules UIKit (`UIHostingConfiguration`), avec des conseils sur la gestion de l'état et le dimensionnement automatique. Enfin, elle propose une checklist de revue, des méthodes de vérification (comme `_printChanges()` et Instruments), et des références aux sessions WWDC pertinentes.

Compatible avec~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/gtokman/skills/tree/HEAD/swiftui-rendering

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Rendu SwiftUI

SwiftUI examine trois choses dans votre code : l'identité (est-ce le même élément qu'avant ?), la durée de vie (combien de temps cette identité dure — et avec elle, @State), et les dépendances (quelles entrées, lorsqu'elles changent, font que body s'exécute à nouveau). Presque toutes les questions « pourquoi cela s'est-il réaffiché / a perdu son état / a animé étrangement / a saccadé » relèvent de l'un de ces trois aspects.

Le modèle

  • Les valeurs de vue sont éphémères ; l'identité ne l'est pas. Une structure de vue est créée, comparée avec la valeur précédente, puis jetée. Ce qui persiste, c'est son identité, et le stockage de @State / @StateObject est lié à cette identité : lorsque l'identité change, l'état est remplacé et onAppear/task s'exécutent à nouveau, les images sont rechargées.
  • Deux types d'identité. Explicite : IDs de ForEach, Identifiable, .id(_:). Structurelle : le type et la position d'une vue dans la hiérarchie — chaque branche d'un if/switch est une identité différente.
  • Le graphe de dépendances. Les dépendances d'une vue sont ses propriétés stockées (entrées de son parent), @State, @Environment, @Binding, et les propriétés @Observable que son body lit. Lorsque l'une d'elles change, seules les vues qui en dépendent reçoivent un nouveau body. SwiftUI compare ensuite les nouvelles valeurs des enfants avec les anciennes et s'arrête là où rien n'a changé.
  • Ordre de mise à jour pour une vue invalidée : nouvelle valeur → propriétés dynamiques mises à jour → exécution de body → comparaison des enfants → récursion.

Règles

Identité

  1. N'identifiez jamais par index (ForEach(Array(x.enumerated()), id: \.offset), 0..<count sur des données dynamiques). Tout élément inséré au-dessus décale chaque index suivant : leur état se réinitialise, leurs images se rechargent, les animations vont au mauvais endroit. Utilisez un ID stable et unique.
  2. Les ID doivent être stables et uniques. Pas de UUID() dans un id calculé (nouvelle identité à chaque rendu). Pas de noms qui peuvent se répéter (deux friandises « Biscuit pour chien »). Rendez les répétitions uniques (clé, clé#2) si les données ne peuvent pas le garantir — les IDs en double dans un ForEach se comportent mal.
  3. Pour le contenu généré ou diffusé, dérivez l'identité d'une structure qui ne bouge pas : l'endroit où l'élément commence dans le texte source (le streaming ne fait qu'ajouter), ou la position du modèle + une clé par itération pour la sortie de programme (le modèle de React : modèle:clé). Nommez les éléments non clés (texte libre, paragraphes) d'après l'élément clé qui les précède plus un compteur (e812+2), afin que le nouveau contenu à la fin ne renomme jamais les éléments antérieurs.
  4. Préférez une vue avec des modificateurs changeants plutôt que des branches. if expiré { v.opacité(0.3) } else { v } est deux identités ; v.opacité(expiré ? 0.3 : 1) en est une. Les valeurs « inertes » (opacité 1, padding 0, frame sans tailles, layoutPriority(0), transformEnvironment) ne coûtent presque rien. Utilisez une branche uniquement pour des vues vraiment différentes.
  5. Changez les mises en page avec AnyLayout, pas avec if wrap { FlowLayout { … } } else { HStack { … } } — les enfants conservent leur identité et leur état.
  6. Évitez AnyView. Il masque la structure de type à SwiftUI (différence moins bonne, et dans List/Table, force la construction de chaque ligne pour les compter). Utilisez des fonctions @ViewBuilder, switch, des génériques.
  7. Modélisez les types d'éléments comme une énumération et utilisez switch dessus dans la vue de distribution, sans default : un nouveau cas ne peut pas être ignoré silencieusement.

Dépendances

  1. Passez aux vues seulement ce qu'elles utilisent. Une vue qui prend un modèle entier réexécute body lorsqu'un champ change ; passez l'image, pas le chien. Extrayez des sous-vues pour limiter les dépendances — avec discernement, pas pour chaque ligne.
  2. @Observable suit ce que body lit. Lire une propriété dans une fermeture d'action ne crée pas de dépendance ; la lire dans body en crée une. Placez l'état que la vue affiche dans le modèle et lisez-le dans body uniquement là où c'est nécessaire.
  3. Ne mettez jamais une nouvelle fermeture (ou une autre valeur non comparable) dans l'environnement à chaque rendu — par exemple .environment(\.actions, Actions { … }) dans un body. Elle ne peut pas être comparée, donc chaque lecteur est invalidé à chaque mise à jour du parent. Mettez plutôt un objet stable dans l'environnement (.environment(modèle) avec @Observable, lu comme @Environment(Model.self) var modèle: Model?), et appelez-le depuis les actions.
  4. Ne notifiez pas lorsque rien n'a changé. Une propriété @Observable notifie à chaque définition. Protégez : if nouveau != valeur { valeur = nouveau } — important lorsque vous reconstruisez un état dérivé à chaque morceau ou tic.
  5. Vues Equatable pour ignorer les sous-arbres inchangés. Conformez la vue (uniquement possible lorsqu'elle n'a pas d'encapsuleurs de propriétés) et appliquez .equatable() là où un parent se réaffiche souvent avec des enfants majoritairement inchangés (une liste de blocs pendant le flux de contenu). Écrivez un == personnalisé ne comparant que ce que la vue affiche. ⚠️ .equatable() masque le layoutPriority de l'enfant à son conteneur. À l'intérieur d'un HStack/d'une ligne, un enfant layoutPriority(1) enveloppé dans EquatableView cesse de gagner de l'espace supplémentaire et la ligne se dispose différemment. Utilisez .equatable() dans les listes verticales de blocs, pas sur les enfants de lignes/piles qui dépendent de la priorité.

body peu coûteux

  1. Pas d'analyse, de décodage, de filtrage, de tri ou de travail d'allocation lourd dans body. Il s'exécute à chaque invalidation. Précalculez dans le modèle, mettez en cache (un NSCache clé par entrée), ou remontez-le à la vue dont les entrées changent réellement.
  2. Séparez les entrées coûteuses de l'état qui change rapidement. Si une vue décode des données et possède un @State qui change souvent (glisser, survoler, frotter), le décodage se réexécute à chaque changement. Décodez dans un parent, passez le résultat à un enfant qui possède le @State.
  3. Le travail lent va dans .task, pas dans init ou body. Rendez-le async ; ne bloquez jamais dans la valeur initiale d'une propriété dynamique.
  4. Mettez en cache le formatage coûteux — AttributedString(markdown:), interpolation de chaînes sur les chemins chauds, recherches dans les bundles.

Listes

  1. List/Table rassemble tous les IDs au départ. Gardez la génération d'ID bon marché et donnez à chaque élément ForEach un nombre constant de vues — pas de filtres if ou de AnyView à l'intérieur de la fermeture de ligne (filtrez les données, dans le modèle, pas en ligne dans body). Les ForEach imbriqués conviennent pour les sections.

SwiftUI dans les cellules UIKit (UIHostingConfiguration)

  • Les cellules sont réutilisées, et l'état de la vue SwiftUI peut être reporté sur l'élément suivant affiché dans cette cellule. Donnez à la racine .id(item.id) afin qu'un élément différent reçoive un arbre frais — et conservez l'état visible par l'utilisateur (coché, développé, sélection) dans le modèle, clé par des IDs stables, pas dans @State, sinon il est perdu/mal appliqué lors de la réutilisation et du défilement.
  • La reconfiguration d'une cellule reconstruit la valeur de la vue racine. Si le contenu est piloté par un modèle observé, rendez la vue racine Equatable sur ce qu'elle affiche (pas sur les entrées brutes qui alimentent uniquement le modèle) et appliquez .equatable(), afin que la reconfigure-par-mise-à-jour ne réexécute pas tout l'arbre — l'observation effectue la mise à jour réelle.
  • L'auto-dimensionnement mesure avec une hauteur ouverte. Les vues sans taille idéale s'y réduisent à zéro : Canvas, formes, GeometryReader. .aspectRatio(r, contentMode: .fit) ne suffit pas — donnez-leur une taille avec un petit Layout personnalisé (largeur à partir de la proposition, hauteur = largeur / ratio ; ignorez les hauteurs de sondage).
  • .aspectRatio(nil, contentMode:) verrouille une vue à la taille idéale de son contenu — les cartes cessent de remplir leur ligne. Appliquez .aspectRatio uniquement lorsque vous avez un ratio.

Liste de vérification pour la revue

  • Chaque ForEach a des IDs stables et uniques — pas de \.offset, pas de UUID() calculé.
  • Pas de AnyView ; des branches uniquement pour des vues vraiment différentes ; AnyLayout pour les changements de mise en page.
  • Les vues reçoivent uniquement les données qu'elles affichent.
  • Rien de non comparable n'est créé par rendu dans l'environnement.
  • body ne fait pas d'analyse/décodage/filtrage ; les entrées coûteuses sont séparées du @State rapide.
  • Les propriétés observables ne sont pas réinitialisées à des valeurs égales.
  • .equatable() là où cela fait gagner du travail — et pas sur les enfants qui utilisent layoutPriority.
  • Cellules hébergées : racine .id(item.id), état visible dans le modèle, racine Equatable.
  • Les vues sans taille idéale en reçoivent une avant d'être placées dans des cellules à auto-dimensionnement.

Vérification

  • let _ = Self._printChanges() en haut d'un body enregistre la raison de son exécution (@self, @identity, ou la propriété qui a changé). Débogage uniquement — supprimez avant la publication.
  • Instruments → modèle SwiftUI pour les comptes de body et les mises à jour lentes ; Hangs/Hitches pour les effets.
  • Test d'identité : affichez chaque préfixe du contenu diffusé et affirmez que l'ID de chaque élément terminé apparaît inchangé dans le rendu final ; après les interactions, affirmez que les éléments survivants ont conservé leurs IDs ; affirmez qu'il n'y a pas d'IDs de frères en double.
  • Diff de pixels avant/après un refactoring de rendu : affichez les vues hors écran sur macOS avec ImageRenderer (même code, .frame(width:), proposedSize avec hauteur nulle) et comparez les PNG. Cela détecte les régressions de mise en page comme celle de EquatableView/layoutPriority. (Map, ScrollView et les images distantes ne s'affichent pas hors écran ; comparez tout le reste.) Divisez une différence pour la trouver en activant un fichier modifié à la fois.

Références

  • references/case-study.md — le rendu de réponse AddToChatMenu (balisage généré par modèle diffusé dans des cellules de collection view) : chaque problème trouvé selon ces règles et sa correction.
  • WWDC21 Demystify SwiftUI (session 10022) ; WWDC23 Demystify SwiftUI performance (session 10160).

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

Skills associés