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/@StateObjectest lié à cette identité : lorsque l'identité change, l'état est remplacé etonAppear/tasks'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'unif/switchest 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@Observableque sonbodylit. Lorsque l'une d'elles change, seules les vues qui en dépendent reçoivent un nouveaubody. 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é
- N'identifiez jamais par index (
ForEach(Array(x.enumerated()), id: \.offset),0..<countsur 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. - Les ID doivent être stables et uniques. Pas de
UUID()dans unidcalculé (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 unForEachse comportent mal. - 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. - 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,framesans tailles,layoutPriority(0),transformEnvironment) ne coûtent presque rien. Utilisez une branche uniquement pour des vues vraiment différentes. - Changez les mises en page avec
AnyLayout, pas avecif wrap { FlowLayout { … } } else { HStack { … } }— les enfants conservent leur identité et leur état. - Évitez
AnyView. Il masque la structure de type à SwiftUI (différence moins bonne, et dansList/Table, force la construction de chaque ligne pour les compter). Utilisez des fonctions@ViewBuilder,switch, des génériques. - Modélisez les types d'éléments comme une énumération et utilisez
switchdessus dans la vue de distribution, sansdefault: un nouveau cas ne peut pas être ignoré silencieusement.
Dépendances
- Passez aux vues seulement ce qu'elles utilisent. Une vue qui prend un modèle entier réexécute
bodylorsqu'un champ change ; passez l'image, pas le chien. Extrayez des sous-vues pour limiter les dépendances — avec discernement, pas pour chaque ligne. @Observablesuit ce quebodylit. Lire une propriété dans une fermeture d'action ne crée pas de dépendance ; la lire dansbodyen crée une. Placez l'état que la vue affiche dans le modèle et lisez-le dansbodyuniquement là où c'est nécessaire.- Ne mettez jamais une nouvelle fermeture (ou une autre valeur non comparable) dans l'environnement à chaque rendu — par exemple
.environment(\.actions, Actions { … })dans unbody. 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. - Ne notifiez pas lorsque rien n'a changé. Une propriété
@Observablenotifie à chaque définition. Protégez :if nouveau != valeur { valeur = nouveau }— important lorsque vous reconstruisez un état dérivé à chaque morceau ou tic. - Vues
Equatablepour 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 lelayoutPriorityde l'enfant à son conteneur. À l'intérieur d'unHStack/d'une ligne, un enfantlayoutPriority(1)enveloppé dansEquatableViewcesse 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
- 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 (unNSCacheclé par entrée), ou remontez-le à la vue dont les entrées changent réellement. - 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
@Statequi 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. - Le travail lent va dans
.task, pas dansinitoubody. Rendez-leasync; ne bloquez jamais dans la valeur initiale d'une propriété dynamique. - Mettez en cache le formatage coûteux —
AttributedString(markdown:), interpolation de chaînes sur les chemins chauds, recherches dans les bundles.
Listes
List/Tablerassemble tous les IDs au départ. Gardez la génération d'ID bon marché et donnez à chaque élémentForEachun nombre constant de vues — pas de filtresifou deAnyViewà l'intérieur de la fermeture de ligne (filtrez les données, dans le modèle, pas en ligne dansbody). LesForEachimbriqué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
Equatablesur 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 petitLayoutpersonnalisé (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.aspectRatiouniquement lorsque vous avez un ratio.
Liste de vérification pour la revue
- Chaque
ForEacha des IDs stables et uniques — pas de\.offset, pas deUUID()calculé. - Pas de
AnyView; des branches uniquement pour des vues vraiment différentes ;AnyLayoutpour 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.
-
bodyne fait pas d'analyse/décodage/filtrage ; les entrées coûteuses sont séparées du@Staterapide. - 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 utilisentlayoutPriority. - 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'unbodyenregistre 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:),proposedSizeavec hauteur nulle) et comparez les PNG. Cela détecte les régressions de mise en page comme celle deEquatableView/layoutPriority. (Map,ScrollViewet 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).