Pour une démonstration en vidéo, regardez la vidéo suivante :
Dans une application iOS, il est souvent nécessaire d’afficher des données sous forme de tableau ou de collection d’éléments visuels alignés. Par exemple :
Les stacks (HStack, VStack) et les Listes (List) permettent d’afficher des éléments en ligne ou en colonne, mais ils ne sont pas adaptés pour organiser des données en grille.
SwiftUI propose donc LazyVGrid et LazyHGrid, deux vues permettant d’organiser le contenu sous forme de grille tout en optimisant les performances.
Les vues LazyVGrid et LazyHGrid sont dites paresseuses (lazy) car elles ne créent des cellules que lorsque celles-ci sont visibles à l’écran. Dès qu’une cellule disparaît de l’écran (par exemple en faisant défiler la liste), SwiftUI la supprime temporairement pour économiser la mémoire et améliorer les performances.
Cela permet d’afficher un nombre potentiellement infini d’éléments sans ralentir l’application.
| LazyVGrid | LazyHGrid |
|---|---|
| Organise les éléments en colonnes | Organise les éléments en lignes |
| Défilement vertical | Défilement horizontal |
| Utilise une liste de GridItem pour définir les colonnes | Utilise une liste de GridItem pour définir les lignes |
Une LazyVGrid s’écrit comme ceci :
LazyVGrid(
columns: [GridItem], // Définition des colonnes
alignment: .center, // Alignement horizontal des éléments (optionnel)
spacing: 10, // Espacement entre les éléments (optionnel)
pinnedViews: [] // Éléments "pinnés" (optionnel)
) {
// Contenu de la grille
}
Une LazyHGrid s’écrit de manière similaire, mais en définissant des lignes plutôt que des colonnes :
LazyHGrid(
rows: [GridItem], // Définition des lignes
alignment: .top, // Alignement vertical des éléments (optionnel)
spacing: 10, // Espacement entre les éléments (optionnel)
pinnedViews: [] // Éléments "pinnés" (optionnel)
) {
// Contenu de la grille
}
GridItem est utilisé pour définir comment les colonnes (dans une LazyVGrid) ou les lignes (dans une LazyHGrid) doivent être configurées.
| Type de GridItem | Description |
|---|---|
| .fixed(CGFloat) | Définit une colonne/ligne de largeur/hauteur fixe. |
| .flexible(minimum: CGFloat, maximum: CGFloat) | Ajuste dynamiquement la taille en fonction de l’espace disponible. |
| .adaptive(minimum: CGFloat) | Crée autant de colonnes/lignes que possible, respectant une taille minimale. |
Grille avec trois colonnes de largeur fixe (100 points chacune) :
let gridItems = [
GridItem(.fixed(100)),
GridItem(.fixed(100)),
GridItem(.fixed(100))
]
Grille avec trois colonnes flexibles (répartition équitable de l’espace disponible) :
let gridItems = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
Grille adaptative (autant de colonnes que possible, largeur min. de 50 points) :
let gridItems = [
GridItem(.adaptive(minimum: 50))
]
Créons une grille contenant 9 cellules colorées.
import SwiftUI
struct ContentView: View {
private let colors: [Color] = [.blue, .yellow, .green]
// Définition des colonnes
private let gridItems = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
var body: some View {
LazyVGrid(columns: gridItems, spacing: 5) {
ForEach(0..<9, id: \.self) { index in
Text("\(index)")
.frame(minWidth: 50, maxWidth: .infinity, minHeight: 100)
.background(colors[index % colors.count])
.cornerRadius(8)
.font(.system(size: 24, weight: .bold))
}
}
.padding()
}
}
Si le nombre d’éléments dépasse la taille de l’écran, il est nécessaire d’ajouter un ScrollView pour permettre le défilement :
var body: some View {
ScrollView {
LazyVGrid(columns: gridItems, spacing: 5) {
ForEach(0..<99, id: \.self) { index in
Text("\(index)")
.frame(minWidth: 50, maxWidth: .infinity, minHeight: 100)
.background(colors[index % colors.count])
.cornerRadius(8)
.font(.system(size: 24, weight: .bold))
}
}
.padding()
}
}
Sans ScrollView, seuls les éléments visibles sur l’écran s’afficheraient et nous ne pourrions pas voir les autres.
Voici un exemple de grille horizontale avec défilement :
struct ContentView: View {
private let colors: [Color] = [.red, .orange, .purple]
private let gridItems = [GridItem(.adaptive(minimum: 50))]
var body: some View {
ScrollView(.horizontal) {
LazyHGrid(rows: gridItems, spacing: 5) {
ForEach(0..<20, id: \.self) { index in
Text("\(index)")
.frame(minWidth: 75, minHeight: 50, maxHeight: .infinity)
.background(colors[index % colors.count])
.cornerRadius(8)
.font(.system(size: 24, weight: .bold))
}
}
.padding()
}
}
}
Pour une démonstration en vidéo, regardez la vidéo suivante :
Dans SwiftUI, nous avons vu que LazyVGrid et LazyHGrid permettent de créer des grilles dynamiques et performantes. Cependant, elles ne sont pas idéales lorsque l’on souhaite une mise en page plus flexible avec un alignement plus précis des cellules.
Dans iOS 17, Apple a introduit Grid et GridRow, qui permettent :
Contrairement à LazyVGrid et LazyHGrid, les grilles Grid et GridRow ne sont pas optimisées pour le défilement. Elles sont plus adaptées aux grilles statiques, comme :
Une Grid est une vue conteneur qui organise ses éléments en lignes (GridRow). Chaque GridRow contient plusieurs cellules, qui sont placées dans des colonnes.
Grid {
GridRow {
// Cellules de la première ligne
}
GridRow {
// Cellules de la deuxième ligne
}
GridRow {
// Cellules de la troisième ligne
}
}
📝 Contrairement à LazyVGrid, on ne définit pas explicitement les colonnes. Elles sont créées automatiquement en fonction du nombre d’éléments présents dans chaque ligne.
Voici une grille de 3 lignes et 5 colonnes qui affiche des nombres :
import SwiftUI
struct ContentView: View {
var body: some View {
Grid {
GridRow {
ForEach(1...5, id: \.self) { index in
CellContent(index: index, colour: .red)
}
}
GridRow {
ForEach(6...10, id: \.self) { index in
CellContent(index: index, colour: .blue)
}
}
GridRow {
ForEach(11...15, id: \.self) { index in
CellContent(index: index, colour: .green)
}
}
}
.padding()
}
}
// Vue réutilisable pour les cellules
struct CellContent: View {
var index: Int
var colour: Color
var body: some View {
Text("\(index)")
.frame(minWidth: 50, maxWidth: .infinity, minHeight: 100)
.background(colour)
.cornerRadius(8)
.font(.system(.largeTitle))
}
}
Une cellule placée en dehors d’un GridRow s’étendra automatiquement sur toute la largeur de la grille.
struct ContentView: View {
var body: some View {
Grid {
GridRow {
ForEach(1...5, id: \.self) { index in
CellContent(index: index, colour: .red)
}
}
GridRow {
ForEach(6...10, id: \.self) { index in
CellContent(index: index, colour: .blue)
}
}
GridRow {
ForEach(11...15, id: \.self) { index in
CellContent(index: index, colour: .green)
}
}
// Une cellule occupant toute la largeur
CellContent(index: 16, colour: .orange)
}
.padding()
}
}
Contrairement à LazyVGrid, où chaque ligne doit avoir le même nombre de colonnes, Grid ajoute automatiquement des cellules vides si nécessaire.
Grid {
GridRow {
ForEach(1...5, id: \.self) { index in
CellContent(index: index, colour: .red)
}
}
GridRow {
ForEach(6...8, id: \.self) { index in
CellContent(index: index, colour: .blue)
}
}
}
📝 Ici, la deuxième ligne a seulement 3 cellules. SwiftUI ajoutera automatiquement 2 cellules vides pour respecter l’alignement.
On peut aussi insérer des cellules vides en utilisant Color.clear et .gridCellUnsizedAxes().
GridRow {
ForEach(1...5, id: \.self) { index in
if index % 2 == 1 {
CellContent(index: index, colour: .red)
} else {
Color.clear
.gridCellUnsizedAxes([.horizontal, .vertical])
}
}
}
Une cellule peut s’étendre sur plusieurs colonnes grâce à .gridCellColumns().
GridRow {
CellContent(index: 17, colour: .orange)
.gridCellColumns(2) // Occupe 2 colonnes
CellContent(index: 18, colour: .indigo)
.gridCellColumns(3) // Occupe 3 colonnes
}
SwiftUI permet d’aligner les cellules au sein de la grille.
Grid(alignment: .topLeading) {
GridRow(alignment: .bottom) {
GridRow(alignment: .bottom) {
Pour une démonstration en vidéo, regardez la vidéo suivante :
Dans SwiftUI, les containers comme VStack, HStack, LazyVGrid et Grid permettent d’organiser les vues efficacement. Cependant, il arrive que l’on ait besoin de créer un container sur mesure qui :
👉 Pour cela, SwiftUI propose le ViewBuilder, un outil puissant permettant de construire des containers personnalisés flexibles et réutilisables.
Un ViewBuilder est un attribut SwiftUI qui permet à une fonction ou à une structure d’accepter plusieurs sous-vues en paramètre sans nécessiter un return explicite.
struct ExempleView: View {
var body: some View {
VStack {
Text("Bonjour")
Text("Bienvenue sur SwiftUI")
}
}
}
📝 Ici, SwiftUI interprète automatiquement que VStack contient plusieurs vues sans nécessiter de retour explicite.
Le ViewBuilder permet d’appliquer cette même logique à nos propres containers.
Un container personnalisé est une structure SwiftUI qui prend des sous-vues et les affiche dans une mise en page spécifique.
struct CustomContainer< Content: View >: View {
@ViewBuilder var content: Content // Le paramètre content peut contenir plusieurs vues
var body: some View {
VStack { // On affiche les vues passées en paramètre
content
}
.padding()
.background(Color.gray.opacity(0.2))
.cornerRadius(10)
}
}
Maintenant que nous avons notre CustomContainer, nous pouvons l’utiliser comme n’importe quelle vue :
struct ContentView: View {
var body: some View {
CustomContainer {
Text("Titre")
.font(.title)
Text("Ceci est un texte dans un container personnalisé.")
.font(.body)
}
}
}
💡 Ici, CustomContainer agit comme un VStack personnalisé, ajoutant automatiquement du padding, un fond gris et des bords arrondis à son contenu.
Nous pouvons améliorer notre container pour lui permettre de :
struct CustomContainer< Content: View >: View {
var backgroundColor: Color
var alignment: HorizontalAlignment
@ViewBuilder var content: Content
var body: some View {
VStack(alignment: alignment) {
content
}
.padding()
.background(backgroundColor)
.cornerRadius(10)
}
}
struct ContentView: View {
var body: some View {
CustomContainer(backgroundColor: .blue, alignment: .leading) {
Text("Titre")
.font(.title)
.foregroundColor(.white)
Text("Texte aligné à gauche dans un container personnalisé.")
.font(.body)
.foregroundColor(.white)
}
}
}
📝 Ici, nous avons ajouté deux paramètres :
Un container peut aussi contenir des animations pour modifier son apparence en fonction d’une interaction.
struct AnimatingContainer< Content: View >: View {
@State private var isTapped = false
@ViewBuilder var content: Content
var body: some View {
VStack {
content
}
.padding()
.background(isTapped ? Color.red : Color.blue)
.cornerRadius(10)
.onTapGesture {
withAnimation {
isTapped.toggle()
}
}
}
}
struct ContentView: View {
var body: some View {
AnimatingContainer {
Text("Cliquez-moi")
.font(.title)
.foregroundColor(.white)
}
}
}
✅ Résultat : Chaque fois que l’utilisateur clique sur le container, la couleur change avec une animation fluide.
Pour une démonstration en vidéo, regardez la vidéo suivante :
Dans SwiftUI, nous avons des containers intégrés comme VStack, HStack, List et LazyVGrid. Mais parfois, nous avons besoin de regrouper et positionner des vues avec une logique personnalisée, par exemple :
💡 SwiftUI nous permet de créer nos propres containers en utilisant ViewBuilder.
Nous allons créer un container “CheckList” qui accepte plusieurs éléments et les regroupe en sections avec en-têtes.
Nous créons d’abord une vue ChecklistSectionHeader pour afficher un titre avant chaque section.
struct ChecklistSectionHeader< Content: View >: View {
@ViewBuilder var content: Content
var body: some View {
HStack {
content
.font(.largeTitle)
.fontWeight(.bold)
}
.padding()
.frame(maxWidth: .infinity)
.background(Color.purple.opacity(0.2))
.cornerRadius(10)
}
}
Nous allons maintenant créer le container CheckList, qui :
struct CheckList< Content: View >: View {
@ViewBuilder var content: Content
var body: some View {
ScrollView(.vertical) {
VStack(spacing: 20) {
ForEach(sections: content) { section in
if !section.header.isEmpty {
ChecklistSectionHeader { section.header }
}
ForEach(subviews: section.content) { subview in
CheckItemView { subview }
}
}
}
.padding()
}
}
}
Les éléments de la CheckList doivent être interactifs, avec une icône de validation qui change lorsqu’on appuie.
struct CheckItemView< Content: View >: View {
@ViewBuilder let content: Content
@State private var isChecked: Bool = false
var body: some View {
HStack {
content
.font(.title)
.fontWeight(.bold)
Spacer()
Image(systemName: isChecked ? "checkmark.circle.fill" : "circle")
.foregroundColor(isChecked ? .green : .gray)
.font(.largeTitle)
.onTapGesture {
withAnimation {
isChecked.toggle()
}
}
}
.padding()
.background(Color.white)
.cornerRadius(10)
.shadow(radius: 3)
}
}
Nous pouvons maintenant utiliser CheckList pour afficher une liste d’éléments groupés en sections.
struct ContentView: View {
let washerTasks = ["Remplacer le joint", "Tester les suspensions", "Nettoyer le filtre"]
let dryerTasks = ["Nettoyer le filtre à charpie", "Vérifier l’évacuation", "Remplacer les rouleaux"]
var body: some View {
CheckList {
Section("\(Image(systemName: "washer.fill")) Lave-linge") {
ForEach(washerTasks, id: \.self) { task in
Text(task)
}
}
Section("\(Image(systemName: "dryer.fill")) Sèche-linge") {
ForEach(dryerTasks, id: \.self) { task in
Text(task)
}
}
}
}
}