Image de présentation d’un écran iOS illustrant les shapes en SwiftUI, plusieurs forme de différentes couleurs sont présentes

Les listes

Image de présentation d’un écran iOS illustrant les shapes en SwiftUI, plusieurs forme de différentes couleurs sont présentes

Les listes

Sur la plupart des applications, tu retrouveras des collections de données affichées sous forme de liste. Vinted affiche des articles d’occasion, Instagram propose un mur de photos, et l’application Messages d’iOS présente la liste des conversations. Dès que tu dois afficher une collection de données à un utilisateur, le composant liste sera ton meilleur allié.

SwiftUI nous fournit deux solutions pour afficher et parcourir ces listes efficacement :

List Un composant optimisé pour afficher des listes de manière performante, avec une gestion automatique du défilement, des séparateurs et de l’affichage mémoire. On peut le personnaliser, mais dans certains cas, ses comportements natifs peuvent être limitants.

ForEach Un constructeur de vues (ce n’est pas une vue en soi, mais il génère dynamiquement des vues en fonction d’une collection). Il permet d’afficher tes éléments avec la mise en page de ton choix, verticalement, horizontalement, en grille, ou même en superposition (ZStack).

L’un ou l’autre sera privilégié selon tes besoins, et ce cours va t’aider à bien comprendre leurs différences et leurs cas d’utilisation

Des données unique

Avant de commencer, on va apprendre à gérer correctement nos données avec SwiftUI. En programmation, en général, quand on va manipuler des données, on va faire en sorte que celles-ci soient uniques. Prends pour exemple ta propre personne, tu as un nom, un prénom, une date de naissance, tu es censé être unique. Cependant, tu n’es pas à l’abri d’avoir un homonyme ! C’est pour ça qu’on t’a attribué une carte d’identité avec un identifiant unique à ta naissance. Ce numéro te représente et permet à la société de ne pas te confondre avec quelqu’un d’autre qui aurait le même nom, prénom et âge que toi.

En programmation, c’est exactement pareil. Dans un programme, rien ne me garantit que je ne vais jamais me retrouver avec une donnée jumelle. Pour me prémunir de ce cas de figure, je peux donner un identifiant unique à chaque donnée.

Pour commencer à travailler sur une liste, on va avoir besoin d’une collection de données, et ça tombe bien, on a déjà vu ça dans la formation Swift Starter Pack ! (Je te mets le lien ici si tu veux te rafraîchir la mémoire ).

Donc, en clair, je vais ici créer une structure pour définir un modèle de données, puis je vais faire un tableau qui va instancier ces données. Cependant, je vais ajouter un protocole à ma structure (idem, je te mets un lien vers le cours sur les protocoles ici). Il s’agit du protocole natif Identifiable, qui permet d’imposer une propriété id à ma structure.

Je peux gérer moi-même le contenu de cette propriété, ce qui sera utile dans certains cas qu’on verra plus tard. Mais pour l’instant, on va utiliser une méthode native de Swift : UUID(), qui va générer un identifiant unique pour chaque instance automatiquement.

C’est une pratique efficace de laisser l’ordinateur gérer ça, car il est très performant pour attribuer des identifiants uniques, là où nous, simples humains, pourrions facilement nous tromper et attribuer accidentellement le même identifiant à deux instances différentes, par exemple.

Allez, je te montre comment faire !

struct Animal: Identifiable {
    let id = UUID()
    let name: String
    let description: String
    let icon: String
    let color: Color
}

 let animals = [
        Animal(name: "Chien", description: "Fidele compagnon des humains.", icon: "pawprint.fill", color: .orange),
        Animal(name: "Fourmi", description: "Insecte des souterrains.", icon: "ant.fill", color: .black),
        Animal(name: "Rouge gorge", description: "Un oiseau majestueux.", icon: "bird.fill", color: .red),
        Animal(name: "Chat", description: "Félin agile et redoutable.", icon: "cat.fill", color: .mint),
        Animal(name: "Tortue", description: "Lent et sage animal terrestre.", icon: "tortoise.fill", color: .green)
 ]

J’ai créé une struct Animal, qui implémente le protocole Identifiable. Ça me permet d’ajouter la propriété var id = UUID(), qui génère automatiquement un identifiant unique pour chaque instance. Désormais, toutes les instances que je vais créer auront un identifiant unique, sans que j’aie à m’en occuper moi-même. Cool, non ?

Cet identifiant est obligatoire, si une donnée n’est pas identifiable, SwiftUI générera une erreur lorsque tu tenteras de l’afficher dans une vue.

 

Le composant List

La List est une vue permettant d’afficher des collections de données sous forme de liste avec un scrolling intégré. Elle prend en paramètre la collection que l’on veut parcourir et afficher, ainsi qu’un index. (C’est en quelque sorte une boucle for déguisée ! Si tu as besoin de revoir ce concept, je te mets le lien du cours ici.)

Ensuite, il me suffit simplement de récupérer les propriétés de chaque élément à l’index correspondant et de les afficher dans le composant souhaité.


 struct AnimalListView: View {
    var body: some View {
        List(animals){ animal in
            HStack{
                Image(systemName: animal.icon)
                Text(animal.name)
            }
        }
    }
}

Liste avec suppression d’éléments

La List offre des fonctionnalités natives pour supprimer des éléments grâce à .onDelete(perform:), qui permet la suppression d’un élément par un simple glissement vers la gauche.

remove(atOffsets:) est une méthode spécifique à SwiftUI qui supprime directement les éléments d’un tableau en utilisant un IndexSet.

On utilise ForEach car il permet d’identifier chaque élément individuellement, ce qui est nécessaire pour .onDelete(perform:) afin de cibler précisément l’élément à supprimer. La List seule ne peut pas gérer cette action.


struct AnimalListView: View {
    @State var animalsArray = animals
    var body: some View {
        List {
            ForEach(animalsArray){ animal in
                HStack{
                    Image(systemName: animal.icon)
                    Text(animal.name)
                }
            }
            .onDelete(perform: deleteItem)
        }
    }
    func deleteItem(at offsets: IndexSet) {
        animalsArray.remove(atOffsets: offsets)
       }
}

SwipeActions

Si tu veux effectuer une action spécifique lorsqu’un élément de la liste est glissé, tu peux utiliser le modificateur .swipeActions, qui te permet de personnaliser l’interaction comme tu le souhaites.

Ici un bouton permettant d’ajouter l’élément aux favoris lors du glissement.


struct AnimalListView: View {
    @State var animalsArray = animals

    var body: some View {
        List {
            ForEach(animalsArray) { animal in
                HStack {
                    Image(systemName: animal.icon)
                    Text(animal.name)
                }
                .swipeActions {
                    Button {
                     //
                    } label: {
                        Label("Favoris", systemImage: "star.fill")
                    }
                    .tint(.yellow)
                }
            }
        }
    }
}

Personnaliser une List

La List est un composant natif d’iOS fourni par défaut avec une mise en forme prédéfinie. Cela dit, SwiftUI permet de le personnaliser en cachant les lignes séparatrices, en changeant la couleur du fond ou encore en modifiant l’apparence des cellules.

Garde tout de même en tête que le composant List peut être limitatif dans certains cas, notamment pour l’affichage de listes horizontales ou en grille. Dans ces situations, on privilégiera ForEach


struct AnimalListView: View {
    var body: some View {
        List(animals, id: \.id) { animal in
            AnimalCellView(animal: animal)
                .listRowBackground(Color.clear)
                .listRowSeparator(.hidden)
        }
        .listStyle(.plain)
        .background(Color.green.opacity(0.1)
            .ignoresSafeArea())
    }
}

struct AnimalCellView: View {
    let animal: Animal
    
    var body: some View {
        HStack(alignment: .top, spacing: 15) {
            Image(systemName: animal.icon)
                .font(.callout)
                .foregroundStyle(.white)
                .padding()
                .background(animal.color)
                .clipShape(Circle())
            VStack(alignment: .leading, spacing: 5) {
                Text(animal.name)
                    .font(.headline)
                    .fontWeight(.bold)
                    .foregroundStyle(.black)
                
                Text(animal.description)
                    .font(.subheadline)
                    .foregroundStyle(.gray)
                    .lineLimit(2)
            }
            Spacer()
        }
        .padding()
        .background(Color.white)
        .cornerRadius(12)
        .shadow(color: .black.opacity(0.1), radius: 5, x: 0, y: 2)
    }
}

Les modificateurs .listRowBackground(Color.clear), .listRowSeparator(.hidden), et .listStyle(.plain) sont spécifiquement dédiés à la personnalisation de l’apparence d’une List (et ne fonctionnent que sur ce composant).

 

Le constructeur de vue ForEach

On l’a déjà aperçu plus haut dans le cours, ForEach est un constructeur de vue, ce qui signifie que ce n’est pas un composant à part entière, contrairement à List. Son rôle est de générer dynamiquement des vues en parcourant une collection de données passée en paramètre.

Par défaut, ForEach ne fait aucune mise en page, il affiche simplement les éléments de manière brute, sans espacement ni structure particulière. C’est justement ce qui fait sa force, là où List peut être limitante pour certaines conceptions, ForEach offre une flexibilité totale. Il permet notamment de créer une liste horizontale, une grille, ou encore d’afficher des éléments dans un ZStack pour des mises en page plus créatives. Les deux exemples qui suivent te présentent un avant et un après de ce que tu peux faire avec un ForEach.


struct ForEachSimpleView: View {
    var body: some View {
        VStack {
            ForEach(animals){ animal in
                HStack {
                    Image(systemName: animal.icon)
                    Text(animal.name)
                }
            }
        }
    }
}

struct ForEachView: View {
    var body: some View {
        ZStack {
            Color.green.opacity(0.3)
            VStack(alignment: .leading, spacing: 15) {
                    VStack(spacing: 15) {
                        ForEach(animals) { animal in
                            AnimalCardView(animal: animal)
                        }
                    }
                    .padding(.horizontal, 20)
                    .padding(.bottom, 20)
            }
        }
         .edgesIgnoringSafeArea(.all)
    }
}

struct AnimalCardView: View {
    let animal: Animal
    
    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 20)
                .fill(Color.white)
                .shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 5)
                .overlay(
                    RoundedRectangle(cornerRadius: 20)
                        .stroke(Color.green, lineWidth: 1)
                )
            HStack {
                VStack(alignment: .leading, spacing: 8) {
                    Text(animal.name)
                        .font(.system(.title3, design: .serif))
                        .fontWeight(.medium)
                        .foregroundStyle(.primary)
                    Text(animal.description)
                        .font(.system(size: 14, weight: .regular, design: .rounded))
                        .foregroundStyle(.gray)
                }
                .padding(.leading, 20)
                Spacer()
                Image(systemName: animal.icon)
                    .font(.system(size: 16))
                    .foregroundStyle(.black)
                    .padding(15)
                    .background(
                        Circle()
                            .stroke(Color.green, lineWidth: 1)
                            .fill(Color.white)
                            .shadow(color: Color.green.opacity(0.3), radius: 5, x: 0, y: 3)
                    )
                    .padding(.trailing, 15)
            }
        }
        .frame(height: 90)
        .rotationEffect(.degrees(-2))
        .padding(.vertical, 5)
    }
}

 Parcourir un Array

Si sur une structure, je peux modéliser une propriété Identifiable pour chaque instance, sur un tableau, je ne peux pas le faire directement.

La solution est de placer un identifiant unique en utilisant id: \.self lorsque le tableau contient des types simples comme String ou Int. Ça permet à SwiftUI de différencier chaque élément en se basant sur sa valeur.


var devices = ["iPhone", "iPad","iPad", "MacBook", "Apple Watch", "iPod Touch", "Apple TV", "Apple Vision Pro"]

struct LoopArrayView: View {
    var body: some View {
        List(devices, id: \.self) { device in
            Text(device)
        }
    }
}

Tips

J’aime beaucoup ce cours ! Tu l’as vu tout au long du programme, nous allons pouvoir utiliser de nombreux outils de base de Swift. Dans ce cours, nous avons retrouvé les arrays, les structs, les protocoles, et surtout les boucles for. Ça permet de mieux comprendre à quoi servent réellement tous ces outils.

Dans la suite de ta montée en compétence en Swift, tu seras amené à maîtriser ces différents outils pour parcourir tes données et les afficher. C’est un socle vraiment important à connaître, et l’un des plus satisfaisants à maîtriser.

Prends le temps de bien comprendre ce que nous avons vu ici, et surtout, pratique, pratique, pratique ! (Je me répète, mais c’est essentiel pour maîtriser la programmation.)

Tips

J’aime beaucoup ce cours ! Tu l’as vu tout au long du programme, nous allons pouvoir utiliser de nombreux outils de base de Swift. Dans ce cours, nous avons retrouvé les arrays, les structs, les protocoles, et surtout les boucles for. Ça permet de mieux comprendre à quoi servent réellement tous ces outils.

Dans la suite de ta montée en compétence en Swift, tu seras amené à maîtriser ces différents outils pour parcourir tes données et les afficher. C’est un socle vraiment important à connaître, et l’un des plus satisfaisants à maîtriser.

Prends le temps de bien comprendre ce que nous avons vu ici, et surtout, pratique, pratique, pratique ! (Je me répète, mais c’est essentiel pour maîtriser la programmation.)