SwiftUI : Construire des interfaces modernes pour iOS

Apprenez à créer des interfaces utilisateur modernes avec SwiftUI : syntaxe déclarative, composants, animations et bonnes pratiques pour iOS 18.

Guide SwiftUI pour créer des interfaces iOS modernes

SwiftUI a transformé le développement d'interfaces sur les plateformes Apple. Avec sa syntaxe déclarative et son intégration native, ce framework permet de créer des applications élégantes en moins de lignes de code que jamais. iOS 18 apporte des améliorations majeures en termes de performance et de nouvelles fonctionnalités.

Pourquoi SwiftUI en 2026 ?

SwiftUI est désormais mature avec iOS 18, offrant un rendu optimisé, une meilleure gestion mémoire, et une intégration UIKit simplifiée. C'est le standard pour les nouvelles applications iOS.

Comprendre le paradigme déclaratif

Avant de coder, comprenons ce qui rend SwiftUI différent. Avec UIKit (l'ancien framework), vous deviez dire à iOS comment construire l'interface étape par étape : "crée un label, positionne-le ici, change sa couleur quand l'utilisateur clique". C'est le paradigme impératif.

SwiftUI fonctionne différemment : vous décrivez ce que vous voulez afficher, et le framework se charge du reste. C'est comme la différence entre donner des instructions GPS tour par tour (impératif) et simplement indiquer la destination (déclaratif).

Votre première View

En SwiftUI, tout élément d'interface est une View. Une View est une structure qui décrit ce qui doit s'afficher à l'écran. Créons notre premier écran avec un titre, un sous-titre et un bouton :

ContentView.swiftswift
import SwiftUI

// Chaque écran est une struct qui implémente le protocole View
struct ContentView: View {

    // La propriété "body" décrit ce que la vue affiche
    // "some View" signifie "un type de View, mais Swift le déduit automatiquement"
    var body: some View {

        // VStack = "Vertical Stack" : empile les éléments verticalement
        // spacing: 20 = 20 points d'espace entre chaque élément
        VStack(spacing: 20) {

            // Un simple texte avec des modificateurs de style
            Text("Bienvenue sur SharpSkill")
                .font(.largeTitle)      // Grande police titre
                .fontWeight(.bold)      // Texte en gras

            Text("Préparez vos entretiens iOS")
                .font(.subheadline)     // Police plus petite
                .foregroundColor(.secondary)  // Couleur grise secondaire

            // Bouton avec une action (closure) et un label
            Button("Commencer") {
                print("Bouton cliqué !")
            }
            .buttonStyle(.borderedProminent)  // Style bouton rempli bleu
        }
        .padding()  // Ajoute des marges autour du VStack
    }
}

Remarquez la structure : on déclare ce qu'on veut (textes, bouton), on les empile verticalement (VStack), et on applique des styles via des modificateurs (.font(), .padding()).

À retenir : En SwiftUI, vous ne "créez" pas des objets UI — vous décrivez l'interface souhaitée. SwiftUI se charge de créer, mettre à jour et détruire les éléments réels.

Les modificateurs : transformer vos vues

Les modificateurs sont des méthodes qu'on chaîne après une vue pour la transformer. Pensez-y comme des filtres Instagram qu'on applique l'un après l'autre. L'ordre compte car chaque modificateur crée une nouvelle vue qui enveloppe la précédente.

Voici un exemple qui illustre pourquoi l'ordre est crucial :

ModifiersOrder.swiftswift
// Exemple 1 : padding PUIS background
Text("SwiftUI")
    .padding()           // 1. Ajoute 16pt d'espace autour du texte
    .background(.blue)   // 2. Le fond bleu couvre le texte + le padding
    .foregroundColor(.white)
// Résultat : texte blanc sur rectangle bleu avec marges

// Exemple 2 : background PUIS padding (ordre inversé !)
Text("SwiftUI")
    .background(.blue)   // 1. Fond bleu serré autour du texte uniquement
    .padding()           // 2. Padding transparent autour du fond bleu
    .foregroundColor(.white)
// Résultat : texte blanc sur petit rectangle bleu, entouré d'espace vide

La différence ? Dans le premier cas, le padding est "à l'intérieur" du background. Dans le second, il est "à l'extérieur". C'est subtil mais essentiel pour maîtriser les layouts.

Organiser vos interfaces avec les Stacks

SwiftUI propose trois conteneurs principaux pour organiser vos vues. Pensez-y comme des boîtes qui arrangent leur contenu différemment.

VStack, HStack et ZStack

  • VStack (Vertical Stack) : empile les éléments du haut vers le bas
  • HStack (Horizontal Stack) : aligne les éléments de gauche à droite
  • ZStack (Z-axis Stack) : superpose les éléments les uns sur les autres

Construisons une carte de profil utilisateur qui combine ces trois stacks. L'objectif : afficher une photo avec un badge de vérification, puis le nom et le rôle de l'utilisateur.

ProfileCard.swiftswift
struct ProfileCard: View {
    var body: some View {
        // HStack principal : photo à gauche, infos à droite
        HStack(spacing: 16) {

            // ZStack pour superposer le badge sur la photo
            ZStack(alignment: .bottomTrailing) {
                // Image de profil (cercle)
                Image("avatar")
                    .resizable()
                    .frame(width: 60, height: 60)
                    .clipShape(Circle())

                // Badge vert "vérifié" en bas à droite
                Image(systemName: "checkmark.circle.fill")
                    .foregroundColor(.green)
                    .background(Circle().fill(.white))  // Cercle blanc derrière
            }

            // VStack pour empiler nom et rôle verticalement
            VStack(alignment: .leading, spacing: 4) {
                Text("Marie Dupont")
                    .font(.headline)

                Text("Développeuse iOS")
                    .font(.subheadline)
                    .foregroundColor(.secondary)
            }

            // Spacer pousse tout vers la gauche
            Spacer()

            // Chevron à droite pour indiquer que c'est cliquable
            Image(systemName: "chevron.right")
                .foregroundColor(.gray)
        }
        .padding()
        .background(Color(.systemGray6))
        .cornerRadius(12)
    }
}

L'astuce ici est le Spacer() : c'est un élément invisible qui prend tout l'espace disponible. Sans lui, les éléments seraient centrés. Avec lui, ils sont poussés vers la gauche et le chevron reste collé à droite.

Astuce Xcode

Utilisez ⌘ + clic sur n'importe quelle vue dans Xcode pour accéder à l'inspecteur visuel. Vous pouvez ajouter des modificateurs sans taper de code !

Gérer l'état avec @State et @Binding

La gestion d'état est le cœur de SwiftUI. L'état, c'est toute donnée qui peut changer et qui doit mettre à jour l'interface. Quand l'état change, SwiftUI re-calcule automatiquement les vues affectées.

@State : l'état local d'une vue

@State est un property wrapper qui dit à SwiftUI : "surveille cette variable, et rafraîchis la vue quand elle change". C'est parfait pour l'état local d'une seule vue.

Créons un compteur interactif pour comprendre le mécanisme :

CounterView.swiftswift
struct CounterView: View {
    // @State crée une "source de vérité" pour cette vue
    // private car l'état ne devrait pas être modifié de l'extérieur
    @State private var count = 0

    var body: some View {
        VStack(spacing: 30) {
            // Ce Text se met à jour automatiquement quand count change
            Text("\(count)")
                .font(.system(size: 72, weight: .bold))

            HStack(spacing: 40) {
                // Bouton décrémente
                Button(action: {
                    count -= 1  // Modifie l'état → la vue se rafraîchit
                }) {
                    Image(systemName: "minus.circle.fill")
                        .font(.largeTitle)
                }

                // Bouton incrémente
                Button(action: {
                    count += 1
                }) {
                    Image(systemName: "plus.circle.fill")
                        .font(.largeTitle)
                }
            }
        }
    }
}

Quand vous appuyez sur un bouton, count change. SwiftUI détecte ce changement et re-exécute body pour mettre à jour l'affichage. Vous n'avez pas à gérer manuellement la mise à jour du label — c'est automatique.

@Binding : partager l'état entre vues

Parfois, une vue enfant doit modifier l'état d'une vue parente. C'est là qu'intervient @Binding : il crée une connexion bidirectionnelle vers un @State existant.

Prenons un exemple concret : un champ de saisie de nom d'utilisateur qui valide en temps réel.

UsernameValidation.swiftswift
// Vue parente : possède l'état
struct SignupForm: View {
    @State private var username = ""       // Source de vérité
    @State private var isValid = false     // État de validation

    var body: some View {
        VStack(spacing: 20) {
            // On passe des BINDINGS (avec $) à la vue enfant
            UsernameField(username: $username, isValid: $isValid)

            Button("Créer mon compte") {
                // Soumettre le formulaire
            }
            .disabled(!isValid)  // Désactivé si invalide
            .buttonStyle(.borderedProminent)
        }
        .padding()
    }
}

La vue enfant reçoit des bindings et peut les modifier :

swift
// Vue enfant : reçoit et modifie l'état via @Binding
struct UsernameField: View {
    @Binding var username: String   // Connexion vers le @State parent
    @Binding var isValid: Bool

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            TextField("Nom d'utilisateur", text: $username)
                .textFieldStyle(.roundedBorder)
                .onChange(of: username) { oldValue, newValue in
                    // Validation : au moins 3 caractères
                    isValid = newValue.count >= 3
                }

            // Feedback visuel
            HStack {
                Image(systemName: isValid ? "checkmark.circle" : "xmark.circle")
                Text("Minimum 3 caractères")
            }
            .font(.caption)
            .foregroundColor(isValid ? .green : .red)
        }
    }
}

Quand l'utilisateur tape dans le TextField, username est modifié via le binding. Le parent voit ce changement et peut l'utiliser. C'est une communication bidirectionnelle propre.

Attention

N'utilisez @State que pour l'état simple et local. Pour des données partagées entre plusieurs écrans ou de la logique complexe, préférez @Observable (iOS 17+) ou les patterns MVVM.

Prêt à réussir tes entretiens iOS ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Afficher des listes dynamiques

Les listes sont omniprésentes dans les applications mobiles. SwiftUI propose List pour afficher des collections de données avec un style natif iOS (séparateurs, swipe actions, etc.).

Créer une liste simple

Pour afficher une liste, vous avez besoin de deux choses : des données et un moyen de les identifier. Le protocole Identifiable permet à SwiftUI de savoir quelle vue correspond à quelle donnée.

Commençons par définir notre modèle de données :

Models/Interview.swiftswift
// Identifiable permet à SwiftUI de tracker chaque élément
struct Interview: Identifiable {
    let id = UUID()           // Identifiant unique auto-généré
    let technology: String
    let difficulty: String
    let questionCount: Int
}

Maintenant, créons la liste. L'idée est d'itérer sur nos données avec ForEach et de créer une ligne pour chaque élément :

InterviewListView.swiftswift
struct InterviewListView: View {
    // Données à afficher (en vrai, ça viendrait d'une API)
    @State private var interviews = [
        Interview(technology: "iOS", difficulty: "Intermédiaire", questionCount: 25),
        Interview(technology: "Android", difficulty: "Avancé", questionCount: 30),
        Interview(technology: "React", difficulty: "Débutant", questionCount: 20)
    ]

    var body: some View {
        // NavigationStack active la barre de navigation
        NavigationStack {
            List {
                // ForEach itère sur chaque interview
                // Grâce à Identifiable, pas besoin de spécifier id:
                ForEach(interviews) { interview in
                    InterviewRow(interview: interview)
                }
            }
            .navigationTitle("Mes entretiens")
        }
    }
}

Et voici la vue pour chaque ligne, extraite dans son propre composant pour la clarté :

InterviewRow.swiftswift
struct InterviewRow: View {
    let interview: Interview

    var body: some View {
        HStack {
            // Colonne gauche : titre et sous-titre
            VStack(alignment: .leading, spacing: 4) {
                Text(interview.technology)
                    .font(.headline)

                Text(interview.difficulty)
                    .font(.subheadline)
                    .foregroundColor(.secondary)
            }

            Spacer()

            // Badge avec le nombre de questions
            Text("\(interview.questionCount) Q")
                .font(.caption)
                .fontWeight(.medium)
                .padding(.horizontal, 8)
                .padding(.vertical, 4)
                .background(Color.blue.opacity(0.1))
                .cornerRadius(8)
        }
        .padding(.vertical, 4)
    }
}

En extrayant InterviewRow dans sa propre struct, le code est plus lisible et réutilisable. C'est une bonne pratique SwiftUI.

Ajouter des actions : suppression et réorganisation

Les listes iOS permettent naturellement de supprimer (swipe gauche) et réorganiser (glisser-déposer) les éléments. SwiftUI rend ça trivial avec .onDelete et .onMove :

InterviewListView.swift (version améliorée)swift
struct InterviewListView: View {
    @State private var interviews = [/* ... données ... */]

    var body: some View {
        NavigationStack {
            List {
                ForEach(interviews) { interview in
                    InterviewRow(interview: interview)
                }
                // Swipe pour supprimer
                .onDelete(perform: deleteInterview)
                // Glisser-déposer pour réorganiser
                .onMove(perform: moveInterview)
            }
            .navigationTitle("Mes entretiens")
            .toolbar {
                // Bouton "Modifier" qui active le mode édition
                EditButton()
            }
        }
    }

    // Supprime les éléments aux index spécifiés
    private func deleteInterview(at offsets: IndexSet) {
        interviews.remove(atOffsets: offsets)
    }

    // Déplace les éléments d'une position à une autre
    private func moveInterview(from source: IndexSet, to destination: Int) {
        interviews.move(fromOffsets: source, toOffset: destination)
    }
}

Avec seulement 4 lignes de code supplémentaires (.onDelete, .onMove, EditButton, et les deux fonctions), vous avez une liste entièrement interactive. C'est la magie de SwiftUI.

Animer vos interfaces

SwiftUI excelle dans les animations. Contrairement à UIKit où les animations demandaient beaucoup de code, ici tout est déclaratif : vous décrivez l'état final et SwiftUI anime la transition.

Animations implicites avec withAnimation

La façon la plus simple d'animer est d'envelopper un changement d'état dans withAnimation. SwiftUI détecte ce qui change et anime automatiquement les propriétés visuelles affectées.

AnimatedCard.swiftswift
struct AnimatedCard: View {
    @State private var isExpanded = false

    var body: some View {
        VStack {
            RoundedRectangle(cornerRadius: 20)
                .fill(.blue)
                // Les dimensions changent selon l'état
                .frame(
                    width: isExpanded ? 300 : 150,
                    height: isExpanded ? 200 : 100
                )

            Button(isExpanded ? "Réduire" : "Agrandir") {
                // withAnimation anime TOUS les changements visuels
                // qui résultent de ce changement d'état
                withAnimation(.spring(response: 0.5, dampingFraction: 0.7)) {
                    isExpanded.toggle()
                }
            }
            .padding(.top)
        }
    }
}

Quand vous cliquez sur le bouton, isExpanded bascule. Grâce à withAnimation, le changement de taille du rectangle est animé avec un effet de ressort (spring). Vous n'avez pas à spécifier quoi animer — SwiftUI le déduit.

Transitions : animer l'apparition/disparition

Les transitions définissent comment une vue apparaît ou disparaît. Par défaut, c'est un fondu (opacity), mais vous pouvez personnaliser :

TransitionDemo.swiftswift
struct TransitionDemo: View {
    @State private var showDetails = false

    var body: some View {
        VStack(spacing: 20) {
            Button("Afficher les détails") {
                withAnimation(.easeInOut(duration: 0.3)) {
                    showDetails.toggle()
                }
            }

            // Cette vue apparaît/disparaît avec une transition
            if showDetails {
                DetailCard()
                    // Transition asymétrique : différente à l'entrée et à la sortie
                    .transition(
                        .asymmetric(
                            insertion: .scale.combined(with: .opacity),  // Entrée : zoom + fondu
                            removal: .slide                               // Sortie : glissement
                        )
                    )
            }
        }
    }
}

struct DetailCard: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Détails de l'entretien")
                .font(.headline)
            Text("25 questions • 45 minutes")
                .foregroundColor(.secondary)
        }
        .padding()
        .background(Color(.systemGray6))
        .cornerRadius(16)
    }
}

Ici, la carte apparaît en zoomant depuis le centre (scale + opacity) et disparaît en glissant sur le côté (slide). Ces micro-animations rendent l'interface vivante et professionnelle.

Performance

SwiftUI optimise automatiquement les animations. Préférez .spring() pour un rendu naturel, et évitez les animations trop longues (> 0.5s) qui frustrent les utilisateurs.

Charger des données asynchrones

Dans une vraie application, vos données viennent souvent d'une API. Swift Concurrency (async/await) s'intègre parfaitement avec SwiftUI grâce au modificateur .task.

Le pattern Loading / Error / Success

Voici le pattern standard pour afficher des données depuis une API. On gère trois états : chargement, erreur, et succès.

AsyncDataView.swiftswift
struct AsyncDataView: View {
    @State private var questions: [Question] = []
    @State private var isLoading = true
    @State private var errorMessage: String?

    var body: some View {
        Group {
            if isLoading {
                // État : chargement en cours
                ProgressView("Chargement...")

            } else if let error = errorMessage {
                // État : erreur
                VStack(spacing: 16) {
                    Image(systemName: "exclamationmark.triangle")
                        .font(.largeTitle)
                        .foregroundColor(.orange)
                    Text(error)
                    Button("Réessayer") {
                        Task { await loadQuestions() }
                    }
                }

            } else {
                // État : succès, afficher les données
                List(questions) { question in
                    Text(question.title)
                }
            }
        }
        // .task se lance automatiquement quand la vue apparaît
        .task {
            await loadQuestions()
        }
        // Pull-to-refresh
        .refreshable {
            await loadQuestions()
        }
    }

    private func loadQuestions() async {
        isLoading = true
        errorMessage = nil

        do {
            // Appel API asynchrone
            questions = try await QuestionService.shared.fetchQuestions()
        } catch {
            errorMessage = "Impossible de charger les questions"
        }

        isLoading = false
    }
}

Le modificateur .task est essentiel : il lance une tâche asynchrone quand la vue apparaît et l'annule automatiquement quand elle disparaît. Pas de fuite mémoire possible.

Conclusion

SwiftUI est devenu incontournable pour le développement iOS moderne. Avec iOS 18, le framework atteint une maturité qui le rend parfaitement adapté aux applications de production.

Ce qu'il faut retenir

  • Paradigme déclaratif : décrivez ce que vous voulez, pas comment le construire
  • @State et @Binding : gérez l'état de façon réactive et propagez-le entre vues
  • Stacks : combinez VStack, HStack et ZStack pour des layouts flexibles
  • List : affichez des collections avec peu de code et des interactions natives
  • Animations : utilisez withAnimation pour des transitions fluides automatiques
  • Async/await : chargez vos données avec .task et gérez les états loading/error

Checklist

  • ✅ Comprendre la différence entre impératif (UIKit) et déclaratif (SwiftUI)
  • ✅ Maîtriser les modificateurs et leur ordre d'application
  • ✅ Savoir quand utiliser @State vs @Binding vs @Observable
  • ✅ Construire des layouts avec les Stacks
  • ✅ Implémenter des listes avec actions (delete, move)
  • ✅ Animer les changements d'état avec withAnimation

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

SwiftUI vous ouvre les portes du développement d'applications élégantes sur tout l'écosystème Apple. Le meilleur moyen d'apprendre reste de pratiquer : créez un petit projet personnel et expérimentez avec chaque concept de cet article !

Tags

#swiftui
#ios
#swift
#ui
#apple

Partager

Articles similaires