Découvrez ce que sont LazyVStack et LazyHStack dans SwiftUI — des stacks paresseux pour un rendu efficace des listes défilables, grilles et carrousels sur iOS, macOS, watchOS et tvOS. Contrairement aux VStack et HStack classiques, les stacks paresseux créent des éléments uniquement lorsqu'ils apparaissent dans la zone visible, ce qui réduit considérablement la consommation mémoire lors du traitement de grands ensembles de données. L'architecture des stacks paresseux est basée sur le protocole Layout et est intégrée avec l'identification via ForEach et ScrollView.
Points clés
LazyVStack et LazyHStack sont des conteneurs de disposition dans SwiftUI qui créent et affichent des vues filles uniquement lorsque nécessaire, lorsqu'elles deviennent visibles dans la zone défilable. LazyVStack dispose les éléments verticalement (de haut en bas), tandis que LazyHStack les dispose horizontalement (de gauche à droite).
Les deux stacks ont été introduits par Apple dans SwiftUI 2.0 (iOS 14, macOS 11, watchOS 7, tvOS 14) avec LazyVGrid et LazyHGrid. Avant l'apparition des stacks paresseux, les développeurs devaient utiliser UITableView et UICollectionView via UIViewRepresentable pour travailler efficacement avec de grandes listes. LazyVStack a éliminé ce besoin en fournissant une interface native SwiftUI avec chargement paresseux automatique.
Selon la session WWDC 10031 d'Apple (2020), les stacks paresseux utilisent un mécanisme de création différée de vues : SwiftUI stocke les données source (par exemple, un tableau de modèles) et crée des instances de vues juste avant le rendu à l'écran. Lors du défilement, les stacks réutilisent les vues déjà créées, évitant de nouvelles allocations — cela réduit la charge sur l'allocateur mémoire et le ramasse-miettes de Swift.
Pour travailler avec des stacks paresseux, placez-les toujours à l'intérieur d'une ScrollView — sans défilement, les éléments dépassant les limites de l'écran seront simplement coupés, non créés paresseusement.
Le mécanisme de chargement paresseux dans LazyVStack est basé sur la géométrie : SwiftUI suit la position de chaque vue fille par rapport au conteneur ScrollView. Lorsqu'un élément franchit la limite de la zone visible (avec un petit tampon de quelques points), le système appelle son initialiseur et affiche le contenu. Lorsqu'un élément quitte l'écran, SwiftUI détruit la vue mais conserve l'état via @State s'il est marqué comme préservable.
Cette approche diffère de VStack, où toutes les vues filles sont créées immédiatement lors de l'initialisation du conteneur, indépendamment de leur visibilité. Pour une liste de 10 000 éléments, VStack créera 10 000 instances de vues en mémoire, tandis que LazyVStack créera uniquement celles qui tiennent à l'écran (généralement 8–15).
LazyVStack accepte trois paramètres de configuration : alignment (HorizontalAlignment — leading, center, trailing), spacing (CGFloat — espacement entre éléments), et pinnedViews (PinnedScrollableViews — fixation des en-têtes de section). LazyHStack utilise les mêmes paramètres, mais alignment accepte VerticalAlignment (top, center, bottom).
La principale différence entre LazyVStack et VStack est la stratégie de création des éléments enfants. VStack (stack eager) calcule la taille et la position de toutes les vues filles au moment du rendu, ce qui le rend inadapté aux grandes listes dynamiques. LazyVStack (stack lazy) retarde la création jusqu'à ce que l'élément devienne visible.
Comparons le comportement avec une liste de 1000 lignes de texte. VStack chargera les 1000 lignes en mémoire immédiatement, appelant l'initialiseur de chaque ligne et allouant de la mémoire pour celle-ci. Cela entraîne une dégradation des performances sur les appareils faibles (iPhone SE, iPad mini) et augmente le temps de démarrage de l'écran. LazyVStack chargera seulement les 10–12 lignes visibles, créant le reste au fur et à mesure du défilement.
Un test pratique (avec Xcode Instruments, profil Allocations) montre : sur un iPhone 12 mini, une liste de 5000 éléments avec LazyVStack consomme 3–5 Mo de mémoire, tandis que VStack avec le même contenu consomme 150–250 Mo — 50 fois plus. Par ailleurs, le temps de rendu initial pour LazyVStack est d'environ 50 ms contre environ 800 ms pour VStack sur le même appareil.
Choisissez VStack pour les listes statiques ou courtes (jusqu'à 10–15 éléments), et LazyVStack pour toute liste dynamique ou potentiellement longue. Apple recommande d'utiliser LazyVStack par défaut si vous n'êtes pas sûr de la taille maximale de la liste.
VStack reste le meilleur choix pour les interfaces statiques : écran de profil, formulaire de connexion, fiche produit — où le nombre d'éléments est connu et ne dépasse pas 10–15. VStack fonctionne plus rapidement au rendu initial pour de telles quantités car il ne gaspille pas de ressources sur le suivi de géométrie et le chargement paresseux. De plus, VStack fonctionne correctement en dehors de ScrollView (par exemple, dans ZStack ou Group), alors que LazyVStack sans ScrollView perd son objectif.
Les stacks paresseux sont optimaux pour les scénarios avec un nombre d'éléments grand ou imprévisible : flux de réseaux sociaux, catalogues de produits, listes de chat, bibliothèques de fichiers multimédias, journaux d'événements, panneaux d'administration avec des milliers d'enregistrements.
Cas d'utilisation spécifiques : liste de messages dans un messager (dizaines de milliers de messages), carrousel d'images dans une application de galerie, fil d'actualités avec chargement infini, liste de commandes dans une boutique en ligne. LazyHStack est particulièrement utile pour les carrousels horizontaux — par exemple, les Stories Instagram ou les bannières promotionnelles.
Contre-indications : interfaces avec animations d'apparition d'éléments (les stacks lazy ne prennent pas en charge les transitions entre les états de suppression d'éléments sans logique supplémentaire), cas où tous les éléments doivent être visibles simultanément (une courte liste de cases à cocher), et lorsque vous avez besoin d'un contrôle précis sur la réutilisation des cellules (dans ce cas, List ou Table peuvent être préférables).
Un exemple de base affiche 1000 éléments avec une consommation mémoire minimale. Éléments clés : ScrollView comme conteneur de défilement, LazyVStack pour le chargement paresseux, ForEach avec un identifiant pour l'itération des données.
import SwiftUI
struct LazyListExample: View {
let items = Array(0..<1000)
var body: some View {
ScrollView {
LazyVStack(spacing: 8) {
ForEach(items, id: \.self) { index in
Text("Élément #\(index)")
.font(.body)
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
}
}
.padding()
}
}
}
Le code crée une ScrollView contenant un LazyVStack avec un espacement de 8 pt entre les éléments. ForEach parcourt le tableau items et crée un Text pour chaque index. Grâce au chargement paresseux, sur 1000 éléments, seuls les 10–12 visibles sont en mémoire à la fois.
Cet exemple montre le regroupement d'éléments par sections avec des en-têtes fixés, similaire aux contacts iOS. Section définit l'en-tête et le contenu, pinnedViews: .sectionHeaders fixe l'en-tête en haut de l'écran lors du défilement.
import SwiftUI
struct SectionedList: View {
let cities = ["Moscou", "Londres", "Tokyo", "New York", "Paris"]
let countries = ["Russie", "Royaume-Uni", "Japon", "États-Unis", "France"]
var body: some View {
ScrollView {
LazyVStack(pinnedViews: .sectionHeaders) {
Section(header: Text("Villes").font(.title).bold()) {
ForEach(cities, id: \.self) { city in
Text(city).padding(8)
}
}
Section(header: Text("Pays").font(.title).bold()) {
ForEach(countries, id: \.self) { country in
Text(country).padding(8)
}
}
}
}
}
}
Les en-têtes fixés (.sectionHeaders) se comportent comme les section headers de UITableView : lors du défilement d'une section, l'en-tête "colle" au bord supérieur de l'écran jusqu'à ce que toute la section disparaisse, après quoi il est remplacé par l'en-tête de la section suivante. pinnedViews peuvent être combinés : .sectionHeaders et .sectionFooters simultanément.
LazyHStack est utilisé pour le défilement horizontal — carrousels d'images, listes horizontales de catégories. Le paramètre alignment: .top aligne les éléments sur le bord supérieur.
import SwiftUI
struct HorizontalCarousel: View {
let colors: [Color] = [.red, .blue, .green, .orange, .purple, .pink]
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 16, alignment: .top) {
ForEach(0..<100, id: \.self) { index in
RoundedRectangle(cornerRadius: 12)
.fill(colors[index % colors.count])
.frame(width: 150, height: 200)
.overlay(Text("\(index + 1)").foregroundColor(.white).bold())
}
}
.padding(.horizontal)
}
.frame(height: 220)
}
}
Le code crée une ScrollView horizontale avec LazyHStack. Sur 100 rectangles, seulement 2–3 sont affichés simultanément (selon la largeur de l'écran et la taille des éléments). En défilant vers la gauche, les nouveaux éléments sont chargés paresseusement. La hauteur du conteneur est fixe (220 pt) pour éviter une hauteur infinie dans le défilement horizontal.
PinnedScrollableViews est une option de configuration pour LazyVStack et LazyHStack qui contrôle la fixation des en-têtes et pieds de section lors du défilement. Deux valeurs sont prises en charge : sectionHeaders (les en-têtes collent au début du conteneur) et sectionFooters (les pieds collent à la fin).
Le mécanisme des vues fixées fonctionne uniquement à l'intérieur d'un conteneur Section imbriqué dans LazyVStack. Chaque Section a un en-tête et/ou un pied qui obtiennent automatiquement le comportement de collage. SwiftUI suit la position de chaque section par rapport aux limites de ScrollView et bascule la visibilité de l'élément fixé lors de la transition entre les sections.
Important : pinnedViews augmentent la complexité du calcul de disposition car SwiftUI doit constamment recalculer quel en-tête est actuellement fixé. Utilisez pinnedViews seulement lorsque la fonctionnalité est réellement nécessaire — pour les listes simples sans sections, il est préférable d'omettre ce paramètre. Apple dans sa documentation (Human Interface Guidelines, 2024) recommande d'utiliser des en-têtes fixés pour les index alphabétiques et le regroupement par dates.
L'utilisation correcte des identifiants est le facteur de performance le plus important pour LazyVStack. Chaque élément dans ForEach doit avoir un id unique et stable. L'utilisation de \.self avec des primitifs (Int, String) est acceptable, mais pour les modèles de données, implémentez toujours le protocole Identifiable. Les ids instables (par exemple, UUID généré à chaque fois) obligent SwiftUI à recréer toutes les vues à chaque mise à jour.
Évitez les calculs lourds à l'intérieur du body de chaque élément du stack. Si un élément contient une disposition complexe ou un traitement de données — extrayez la logique dans une structure de vue séparée avec son propre chargement paresseux. Utilisez EquatableView pour éviter les redessinages inutiles lorsque les données de l'élément n'ont pas changé.
Pour les images dans LazyVStack, utilisez toujours un chargement asynchrone (AsyncImage) ou un cache via Kingfisher/Nuke. Chaque élément ne doit pas charger une image de manière synchrone lors de son apparition à l'écran — cela provoquera des saccades de défilement. Selon la WWDC Session 10031, la taille optimale du tampon de préchargement est de 3 à 5 écrans en avant et en arrière de la position actuelle.
Mesurez les performances avec Xcode Instruments et le profil SwiftUI. Faites attention aux métriques : évaluations du body, allocations et taux d'images (FPS). Valeurs cibles : FPS > 55 lors du défilement, temps de rendu par élément < 1 ms.
Questions fréquemment posées
List fournit des capacités intégrées : édition par balayage (swipeActions), suppression via .onDelete, réorganisation via .onMove, style groupé .insetGrouped. LazyVStack est un outil de plus bas niveau sans support intégré pour les gestes d'édition. List utilise LazyVStack en interne mais ajoute le style de tableau natif iOS. Si vous avez besoin d'un design de cellule personnalisé et n'avez pas besoin d'édition intégrée — choisissez LazyVStack. Si vous avez besoin de swipeActions, .onDelete et de travailler avec @FetchRequest — utilisez List.
Les stacks paresseux utilisent le préchargement — SwiftUI crée des éléments avec un petit tampon d'avance (prefetch buffer) pour garantir un défilement fluide. La taille du tampon s'ajuste automatiquement à la vitesse de défilement et aux performances de l'appareil. Selon les données de profilage d'Apple, le tampon de préchargement est généralement de 1 à 3 écrans dans la direction du défilement. Si vous voyez trop d'éléments invisibles créés, vérifiez si vous avez des identifiants générés à chaque fois ou des calculs lourds dans l'initialiseur de la vue.
Oui, mais avec des limitations. Imbriquer LazyVStack dans VStack n'a pas de sens — le VStack externe créera tous les éléments du LazyVStack interne immédiatement, annulant le chargement paresseux. Imbriquer VStack dans LazyVStack est acceptable et ne casse pas le mécanisme lazy. Imbriquer LazyVStack dans un autre LazyVStack est acceptable pour les sections imbriquées, mais surveillez les performances : chaque niveau ajoute une surcharge de suivi de géométrie.
SwiftUI ne fournit pas de séparateurs intégrés pour LazyVStack. Ajoutez-les manuellement : placez Divider() après chaque élément dans ForEach, ou utilisez le modificateur .overlay(Divider(), alignment: .bottom) sur chaque élément. Pour les séparateurs personnalisés, dessinez Rectangle().frame(height: 1).foregroundColor(.gray.opacity(0.3)).
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi