Module 8 - Note de cours

← Retour au module principal

Création de grilles avec LazyVGrid et LazyHGrid

Pour une démonstration en vidéo, regardez la vidéo suivante :

Introduction

Pourquoi utiliser des grilles 🤔

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.

Comprendre LazyVGrid et LazyHGrid

Qu’est-ce qu’une grille paresseuse 🤔

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.

Différence entre LazyVGrid et LazyHGrid

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

Syntaxe de base

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
}

Les GridItems : Définition des colonnes et des lignes

GridItem est utilisé pour définir comment les colonnes (dans une LazyVGrid) ou les lignes (dans une LazyHGrid) doivent être configurées.

Il existe trois types de GridItem :

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.

Exemples de GridItems

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))
]

Exemple d’implémentation d’une LazyVGrid

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()
    }
}

Explication :

Ajout du défilement avec ScrollView

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()
    }
}

Pourquoi utiliser un ScrollView ?

Sans ScrollView, seuls les éléments visibles sur l’écran s’afficheraient et nous ne pourrions pas voir les autres.

Exemple d’utilisation de LazyHGrid

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()
        }
    }
}

Explication :

Conclusion

Grilles avancées avec Grid et GridRow

Pour une démonstration en vidéo, regardez la vidéo suivante :

Introduction

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 :

Structure de base d’une grille avec Grid et GridRow

Syntaxe de base :

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.

Exemple de grille simple

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))
    }
}

Explication :

Ajouter une cellule occupant toute une ligne

Une cellule placée en dehors d’un GridRow s’étendra automatiquement sur toute la largeur de la grille.

Exemple : ajout d’une cellule occupant toute la ligne 4 :

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()
}
}

Explication :

Gestion automatique des cellules vides

Contrairement à LazyVGrid, où chaque ligne doit avoir le même nombre de colonnes, Grid ajoute automatiquement des cellules vides si nécessaire.

Exemple : une ligne avec moins de colonnes :

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.

Ajouter manuellement des cellules vides

On peut aussi insérer des cellules vides en utilisant Color.clear et .gridCellUnsizedAxes().

Exemple : des colonnes vides aux positions paires :

GridRow {
    ForEach(1...5, id: \.self) { index in
        if index % 2 == 1 {
            CellContent(index: index, colour: .red)
        } else {
            Color.clear
                .gridCellUnsizedAxes([.horizontal, .vertical])
        }
    }
}

Explication :

Fusion de colonnes (Col-Spanning)

Une cellule peut s’étendre sur plusieurs colonnes grâce à .gridCellColumns().

Exemple : deux cellules occupant plusieurs colonnes :

GridRow {
    CellContent(index: 17, colour: .orange)
        .gridCellColumns(2) // Occupe 2 colonnes
    CellContent(index: 18, colour: .indigo)
        .gridCellColumns(3) // Occupe 3 colonnes
}

Explication :

Gestion de l’alignement

SwiftUI permet d’aligner les cellules au sein de la grille.

Alignement global de la grille :

Grid(alignment: .topLeading) {

Alignement d’une ligne spécifique :

GridRow(alignment: .bottom) {

Alignement d’une seule colonne :

GridRow(alignment: .bottom) {

Conclusion

Création de Containers Personnalisés avec ViewBuilder en SwiftUI

Pour une démonstration en vidéo, regardez la vidéo suivante :

Introduction : Pourquoi créer un container personnalisé 🤔

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.

Qu’est-ce qu’un ViewBuilder ?

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.

💡 Exemple simple :

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.

Définir un Container Personnalisé avec ViewBuilder

Un container personnalisé est une structure SwiftUI qui prend des sous-vues et les affiche dans une mise en page spécifique.

Syntaxe de base d’un container personnalisé avec ViewBuilder

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)
    }
}

Utilisation d’un container personnalisé

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.

Ajout de paramètres pour plus de flexibilité

Nous pouvons améliorer notre container pour lui permettre de :

Amélioration du container personnalisé :

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)
    }
}

Utilisation avec des paramètres :

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 :

Création d’un container personnalisé avec des animations

Un container peut aussi contenir des animations pour modifier son apparence en fonction d’une interaction.

Exemple : Un container qui change de couleur lorsqu’on clique dessus :

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()
            }
        }
    }
}

Utilisation du container animé :

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.

Conclusion

Création avancée de containers personnalisés en SwiftUI

Pour une démonstration en vidéo, regardez la vidéo suivante :

Rappel : Pourquoi créer un container personnalisé ?

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.

Création d’un container personnalisé avec sections et en-têtes

Objectif :

Nous allons créer un container “CheckList” qui accepte plusieurs éléments et les regroupe en sections avec en-têtes.

Étape 1 : Création d’une section d’en-tête

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)
    }
}

Étape 2 : Création du container principal CheckList

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()
        }
    }
}

Étape 3 : Création d’un élément de liste cochable CheckItemView

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)
    }
}

Étape 4 : Utilisation du container CheckList

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)
                }
            }
        }
    }
}

Résumé des concepts abordés