Que fait Expert SwiftUI ?
Une vue SwiftUI se joue ou se perd sur la propriété de l'état : placer l'état au mauvais endroit et chaque frappe réévalue tout l'arbre, les listes bégayent et les animations se déchirent. Cette compétence construit et révise des vues SwiftUI afin que l'invalidation reste cantonnée aux vues qui lisent réellement les données modifiées, que le budget d'image soit respecté lors du défilement, et que le résultat soit accessible par défaut plutôt que rétrofit.
Procédure opératoire
Travaillez dans cet ordre car la conception de l'état détermine tout ce qui suit – corriger l'état après avoir construit la hiérarchie des vues signifie réécrire la hiérarchie.
Étape 1 : Recueillir les entrées
Collectez avant d'écrire du code de vue. Là où l'utilisateur ne sait pas, utilisez la valeur par défaut et étiquetez-la comme une estimation.
- Appareils cibles et taux de rafraîchissement (par défaut : supposer un iPhone ProMotion 120 Hz plus un appareil de base 60 Hz).
- Forme des données : combien d'éléments dans la plus grande liste, à quelle fréquence les données mutent, si elles arrivent en streaming.
- Carte de propriété : pour chaque élément d'état, quelle vue le crée, quelles vues le lisent, lesquelles le modifient.
- Exigences d'accessibilité au-delà de la base (par défaut : prise en charge complète de Dynamic Type + VoiceOver).
Étape 2 : Choisir la propriété de l'état par valeur
Choisissez le wrapper le plus étroit qui fonctionne :
@State- état local, propriété de la vue, de type valeur. Également le propriétaire correct pour une instance de modèle@Observablecréée par la vue.@Binding- une référence bidirectionnelle à un état détenu ailleurs. Passez une liaison uniquement lorsque l'enfant mute ; passez une valeur simple lorsqu'il ne fait que lire.@Observable(framework Observation) - objets modèle ; les vues n'observent que les propriétés qu'elles lisent, donc l'invalidation est par propriété, pas par objet.@Environment- injection de dépendances vers le bas de l'arbre pour les services et les modèles partagés.
Règle : l'état vit à l'ancêtre commun le plus bas de chaque vue qui le lit ou l'écrit – pas plus bas (il se réinitialise lorsque la vue disparaît) et pas plus haut (il élargit l'invalidation).
Étape 3 : Composer les vues pour limiter l'invalidation
- Gardez
bodypetit ; extrayez les sous-vues afin qu'un changement invalide une ligne, pas l'écran. Unbodyqui dépasse environ 50 lignes ou trois niveaux d'imbrication est un candidat à la scission. - Passez un minimum de données dans les sous-vues – passez
item.title, pas tout le store – afin que seules les vues concernées soient re-rendues. - Utilisez des helpers
@ViewBuilderau lieu de grandes fermetures imbriquées. - Ne faites jamais de travail lourd (formatage, tri, filtrage, décodage) dans
body; calculez-le dans le modèle et passez les valeurs finalisées à la vue.bodydoit rester dans la plage des microsecondes car il peut s'exécuter à chaque image pendant l'animation.
Étape 4 : Listes et budget d'image
Le budget est de 8,3 ms par image à 120 Hz, 16,7 ms à 60 Hz – tout sur le thread principal pour cette image, pas seulement votre ligne. Règles pratiques :
- Utilisez
ListouLazyVStackpour tout ce qui dépasse ~50 lignes afin que les lignes soient rendues à la demande ; uneVStacksimple dans uneScrollViewconstruit chaque ligne à l'avance. - Donnez aux éléments des
idstables provenant de vos données (item.id), jamais des indices de tableau ouUUID()calculés dansbody– des identifiants instables détruisent l'identité des lignes, provoquant des scintillements, une perte de position de défilement et une perte de l'état de la ligne. - Les lignes doivent avoir un coût constant : pas de formateurs de date par ligne ou de décodage d'image ; mettez en cache les formateurs, utilisez
AsyncImageou un pipeline d'image avec cache. - Vérifiez avec le modèle SwiftUI d'Instruments : surveillez les compteurs d'évaluation de
bodydes vues pendant le défilement. Un type de ligne dont lebodys'évalue lorsque l'état non lié change est un bogue de propriété d'état – corrigez l'étape 2, ne mémorisez pas autour.
Étape 5 : Animer à partir de l'état
- Animez à partir de l'état :
withAnimation { model.expanded.toggle() }. - Préférez
.animation(_:value:)ciblé sur une valeur spécifique plutôt que des animations globales implicites, qui animent des choses que vous n'aviez pas prévues. - Utilisez
matchedGeometryEffectpour les transitions d'élément partagé ;transitionpour l'insertion/suppression. - Respectez
accessibilityReduceMotion: remplacez le mouvement par des fades d'opacité lorsqu'il est activé.
Étape 6 : Passage d'accessibilité
- Ajoutez
accessibilityLabel/accessibilityValue; regroupez les éléments associés avecaccessibilityElement(children: .combine)afin qu'une ligne se lise comme une phrase, pas cinq fragments. - Respectez Dynamic Type – styles de texte système, pas de cadres fixes qui coupent le texte ; vérifiez à la plus grande taille d'accessibilité.
- Vérifiez le contraste et ajoutez des variantes
#Previewpour le mode sombre, le Dynamic Type large et les états vide/chargement/erreur avec des exemples de données.
Artéfact travaillé : propriété de l'état, mauvais vs bon
Mauvais – tout l'écran s'invalide à chaque frappe parce que le texte de recherche vit dans un modèle que tout l'arbre observe, et les lignes reçoivent tout le store :
@Observable final class ScreenStore {
var searchText = ""
var items: [Item] = []
}
struct ContactList: View {
@State private var store = ScreenStore()
var body: some View {
VStack {
TextField("Search", text: $store.searchText)
ForEach(store.items) { item in
ContactRow(store: store, item: item) // la ligne observe tout
}
}
}
}
Bon – le texte de recherche est local à la vue, les lignes n'obtiennent que les valeurs qu'elles lisent, et la liste est paresseuse avec des identifiants stables :
struct ContactList: View {
@State private var searchText = ""
@Environment(ContactModel.self) private var model
var body: some View {
List(model.filtered(searchText)) { item in
ContactRow(name: item.name, isOnline: item.isOnline)
}
.searchable(text: $searchText)
}
}
struct ContactRow: View {
let name: String
let isOnline: Bool
var body: some View {
Label(name, systemImage: isOnline ? "circle.fill" : "circle")
}
}
Taper dans la version bonne invalide la fermeture du contenu de List et les lignes dont l'appartenance filtrée a changé – rien d'autre.
Livrable
Produisez du code de vue SwiftUI fonctionnel plus un paragraphe de carte d'état indiquant, pour chaque élément d'état : son propriétaire, son wrapper et quelles vues le lisent vs l'écrivent. Pour les révisions, produisez une liste de constats ordonnés par impact sur le budget d'image, chacun avec la correction.
À NE PAS FAIRE
- Ne jamais muter l'état pendant l'évaluation de
body– cela boucle ou déclenche des avertissements de comportement indéfini. - Ne faites pas de travail asynchrone dans
onAppearavec uneTaskbrute – utilisez.task {}, qui s'annule automatiquement à la disparition. - Ne mettez pas de types de vue (
Color,Image,AnyView) dans les modèles ; les modèles doivent rester testables sans interface utilisateur. - Ne soulevez pas tout l'état vers un seul store au niveau de l'écran "pour simplifier" – c'est exactement le motif qui fait que chaque frappe redessine l'écran.
- Ne masquez pas les bogues d'invalidation avec
equatable()ou la mise en cache avant de corriger la propriété ; le bogue refera surface dans la prochaine vue qui observera le même objet. - N'utilisez pas de cadres fixes sur le texte – Dynamic Type le coupera.
Barre de qualité
Une vue terminée passe lorsque :
- Chaque élément d'état a exactement un propriétaire et le wrapper le plus étroit qui fonctionne.
- Instruments montre que les
bodydes lignes ne s'évaluent que lorsque leurs propres données changent. - Le défilement de la plus grande liste attendue respecte le budget d'image sur l'appareil de base 60 Hz.
- L'état de longue durée est soulevé au-dessus de toute vue qui peut disparaître, afin qu'il survive à la navigation.
- L'évitement du clavier et les zones de sécurité sont gérés (
safeAreaInset,scrollDismissesKeyboard). - VoiceOver lit chaque élément interactif avec une étiquette significative ; la disposition survit à la plus grande taille de Dynamic Type ;
reduceMotionest respecté. - Des aperçus existent pour les états de chargement, vide, erreur, mode sombre et grande taille de police.
Pour un profilage à l'échelle de l'application au-delà d'un seul écran, associez à mobile-perf-profiler.