Développer avec SwiftUI offre une approche déclarative moderne, mais cela demande de comprendre comment fonctionne le rendering SwiftUI pour maintenir des UI performantes.
DÉMYSTIFIER LE CYCLE DE RENDERING SWIFTUI
Pour bien concevoir une interface, il faut d'abord comprendre que le rendering SwiftUI est un cycle. Ce cycle se décompose en quatre étapes logiques :
- Un changement d'état.
- Le recalcul des vues.
- La comparaison des vues.
- L'application des différences.
La notion fondamentale à intégrer est qu'une View est une valeur. Concrètement, la fonction body renvoie l'UI en fonction de la valeur de la vue. Lorsqu'une valeur interne à la View change, la fonction body est ré-exécutée pour produire un UI "à jour". Attention cependant : lorsqu'une valeur modifie une sous vue c'est toute la branche de l'arbre de vue qui se voit évaluée à nouveau.
Qu'est-ce qui déclenche une ré-évaluation de la vue ?
Les invalidations de la vue sont principalement déclenchées par les éléments suivants :
- Un @State change.
- Un @Binding change (venu du parent).
- Un @ObservedObject / @StateObject publie via objectWillChange.
- Un @Environment change.
- Des changements de données observées via @FetchRequest / SwiftData / CoreData.
- Des interactions utilisateurs via des gestures.
- Des animations.
IDENTITY ET DIFFÉRENCES : CE QUI DÉCLENCHE UN REDRAW
Qu'est-ce qui déclenche un redraw de la vue ?
Une fois la réévaluation de la vue effectuée, SwiftUI va faire plusieurs vérifications d'Identity (via les id) et de différences (via comparaison des valeurs des structs).
L'importance de l'Identity :
- Avec des id stables, SwiftUI garde l'état de la vue et la réutilise.
- À l'inverse, avec des id instables, SwiftUI va considérer la vue comme une nouvelle vue, ce qui entraîne la recréation et réinitialisation de l'état.
La gestion des différences d'états : Selon les différences, SwiftUI va adapter son comportement. Il peut garder certaines parties de la vue qui n'ont pas de différences d'états. S'il y a des modifications, il devra recalculer les layout associés, redessiner la vue, voire remplacer la vue complètement.
LES TROIS PHASES À NE PAS CONFONDRE : UPDATE, LAYOUT ET DISPLAY
Il ne faut pas confondre les différentes étapes. Quand body est ré-évalué, il peut y avoir ensuite des phases distinctes :
1. L'Update (Mise à jour)
C'est la mise à jour des propriétés / création logique des vues. Ton body est ré-évalué. SwiftUI produit une nouvelle description de l'UI (un nouvel "arbre" de Views). Ensuite, SwiftUI fait du diffing avec l'arbre précédent (réconciliation) et prépare une liste d'opérations ("mettre à jour telle propriété", "remplacer telle sous-vue", etc.).
- À éviter : les onAppear, onChange et task nombreux.
2. Le Layout (Mise en page)
Il s'agit du calcul des tailles et positions, ce qui peut être coûteux. Une fois que SwiftUI sait quelles vues existent, il doit décider leur taille et position. Ça passe par des cycles de mesure (propose une taille -> la vue répond) puis place (positionnement). Certaines vues déclenchent des calculs de layout plus coûteux : GeometryReader, texte multi-ligne, stacks complexes, contenu dynamique.
- À éviter : les textes riches ou très nombreux (surtout si wrapping) ; les hiérarchies profondes de VStack/HStack/ZStack avec beaucoup de conditionnels ; et surtout, évitez de coupler un GeometryReader et un feedback de taille via PreferenceKey (ça peut créer une boucle taille -> état -> update -> relayout).
3. Display / Rendering (Affichage)
C'est le dessin (CPU/GPU), le compositing, les animations. C'est le moment où l'UI est dessiné et composé à l'écran (GPU). Cela comprend le drawing (CPU/GPU selon les cas), le compositing (assembler les calques), et les animations.
- À éviter : le blur, les materials, les shadows sur de grandes surfaces ; l'usage de mask, clipShape complexe, cornerRadius sur gros calques ; les overlays multiples, transparences empilées ; les images énormes non redimensionnées (texture upload) ; et l'offscreen rendering (certaines combinaisons d'effets).
Le cas particulier du Scroll : au scroll, qu'est-ce qui se passe ? Pendant un scroll, le système fait bouger le contenu (GPU/compositing). Mais si tu as des vues qui dépendent de la position (ex : GeometryReader, PreferenceKey, onAppear / onDisappear fréquents, effets, animations), tu peux forcer des invalidations répétées.
STRATÉGIES ET OPTIMISATIONS CONCRÈTES
Voici les optimisations à faire si vous rencontrez des lenteurs :
- Calculs lourds dans body : déplacez le calcul hors du body (cache, computed properties, ViewModel).
- Propagation de l'état : évitez la recomposition de grosses sous-vues parce que l'état se propage trop haut dans la hiérarchie des vues (ex : passage de l'offset d'une vue jusqu'au Xème parents). Pour cela, veillez à localiser un maximum les @State et à découper les sous vues avec des propriétés qui varient moins.
- Stabiliser les listes : en cas d'Identity instable dans les listes, il faut limiter la recréation totale des vues en ajoutant des id stables. Dans les ForEach, utiliser des Identifiable qui ajoute un id implicite directement.
- Ressources et effets : gérez les images non redimensionnées ou décodées sur le main thread. Limitez les effets type blur, shadows, masks, materials sur de grandes surfaces et attention aux animations déclenchées trop souvent.
DISTINCTION OBSERVE DOBJECT/ STATEOBJECT
Les soucis d'objet Observable sont également à prendre en compte. Considérez cet exemple :

SwiftUI peut recréer une View à tout moment (ce sont des structs, elles sont jetables). Chaque recréation de MyView instancie un nouveau MyViewModel et tu perds ton état, tes subscriptions, tes requêtes en cours.
Les solutions :
- Utiliser @StateObject créé dans la vue elle même pour garantir le lien avec le cycle de vie de la vue. SwiftUI garantit que l'instance est créée une seule fois, même si la View est recréée.

- Utiliser @ObservedObject pour un objet injecté de l'extérieur, qui va gérer le cycle de vie en amont.

CONCLUSION
En résumé, obtenir des UI performantes avec SwiftUI est avant tout une question de logique. Tout s'articule autour d'une bonne compréhension de son cycle de rendering : le changement d'état, le recalcul des vues, leur comparaison, et enfin l'application des différences à l'écran.
Pour ne pas subir les baisses de performances au fil du développement de votre application, gardez ces trois réflexes en tête :
- Maîtrisez votre Identity : utilisez des id stables pour réutiliser l'état de la vue. Cela vous évitera une recréation et une réinitialisation complètes qui pèsent inutilement sur le système.
- Allégez vos phases de rendu : distinguez bien la mise à jour (Update), le calcul des tailles (Layout) et l'affichage final (Display). Évitez d'y accumuler des calculs lourds, des GeometryReader couplés à des PreferenceKey, ou des effets visuels trop intenses sur de grandes surfaces.
- Sécurisez vos données : rappelez-vous que les vues sont des structures jetables. Liez toujours la création de vos objets au cycle de vie de la vue avec @StateObject pour ne pas perdre vos états et vos requêtes en cours à chaque rafraîchissement.
En appliquant ces principes d'optimisation, vous ciblerez les calculs réellement nécessaires et vous offrirez à vos utilisateurs une interface fluide, y compris lors d'interactions exigeantes comme le scroll.
Bastien LEBRUN - Développeur iOS Confirmé
