← Retour au cours
▶ Aperçu gratuit · Leçon offerte

SwiftUI déclaratif : Views, state management, NavigationStack, animations

⏱ 840 min · 🎬 Lecon · 🏆 20 XP
🎬
Vidéo en production
Notre équipe pédagogique tourne actuellement cette leçon avec un·e formateur·rice expert·e. Le contenu textuel ci-dessous est complet et utilisable dès maintenant.

Leçon 2 — SwiftUI déclaratif et gestion d'état

Views, modifiers, state management, NavigationStack, animations — la nouvelle norme UI Apple.

🎯 Objectifs pédagogiques

  • Construire des interfaces avec le paradigme déclaratif SwiftUI et la composition de Views.
  • Maîtriser les property wrappers d'état : @State, @Binding, @ObservedObject, @StateObject, @Environment, @Observable.
  • Naviguer avec NavigationStack (iOS 16+), TabView, sheet, alert, popover.
  • Animer une UI avec .animation, withAnimation, transitions et matchedGeometryEffect.
  • Comparer UIKit vs SwiftUI et savoir quand mixer les deux via UIViewRepresentable.

1. Le paradigme déclaratif

Annoncé à la WWDC 2019 (iOS 13+), SwiftUI est le framework UI moderne d'Apple, inspiré de React, Flutter et Jetpack Compose. Là où UIKit (impératif, MVC, depuis 2007) demande de manipuler chaque vue et son cycle de vie manuellement, SwiftUI décrit l'état souhaité de l'UI à un instant T : le framework s'occupe du diffing et redessine uniquement ce qui change.

Une View SwiftUI est une struct (value type immuable) conforme au protocole View, exposant une propriété calculée body. Le compilateur Swift utilise un result builder (@ViewBuilder) pour assembler les sous-vues en arbre statique typé, vérifié à la compilation.

struct GreetingView: View {
    var name: String

    var body: some View {
        VStack(spacing: 12) {
            Text("Bonjour, \(name)")
                .font(.largeTitle)
                .foregroundStyle(.blue)
            Image(systemName: "hand.wave.fill")
                .imageScale(.large)
        }
        .padding()
    }
}

Les modifiers (.font, .padding, .background...) retournent une nouvelle View. L'ordre compte : .padding().background(Color.red) n'est pas équivalent à .background(Color.red).padding().

"SwiftUI is a modern way to declare user interfaces for any Apple platform. Create beautiful, dynamic apps faster than ever before." — Apple Developer — SwiftUI

2. State management : 6 property wrappers à maîtriser

SwiftUI redessine une View lorsque sa source de vérité change. Cette source est encapsulée dans des property wrappers spécifiques selon la portée et la propriété :

WrapperUsagePortée
@StateÉtat local privé de la View (struct)Une seule View, valeur initialisée à la construction
@BindingRéférence two-way à un @State parentSous-View qui modifie l'état du parent
@StateObjectPossession d'un ObservableObject (créé par cette View)ViewModel instancié et détenu par cette View
@ObservedObjectRéférence à un ObservableObject existant (créé ailleurs)ViewModel reçu en paramètre
@EnvironmentObjectInjection global (DI) via l'environnementSingleton-like, traverse plusieurs niveaux de navigation
@Environment(\.dismiss)Valeurs système (colorScheme, dismiss, locale...)Toute View enfant

iOS 17 introduit la macro @Observable (framework Observation) qui remplace ObservableObject + @Published par un mécanisme plus performant basé sur le tracking automatique. Désormais :

@Observable class UserViewModel {
    var user: User?
    var isLoading = false

    func load() async {
        isLoading = true
        defer { isLoading = false }
        user = try? await API.fetchCurrentUser()
    }
}

struct ProfileView: View {
    @State private var vm = UserViewModel()  // plus de @StateObject !

    var body: some View {
        Group {
            if vm.isLoading { ProgressView() }
            else if let user = vm.user { Text(user.name) }
        }
        .task { await vm.load() }
    }
}

3. Navigation moderne avec NavigationStack

Depuis iOS 16, NavigationStack remplace le NavigationView déprécié. Il gère une pile de destinations typées via NavigationPath, ce qui permet la deep linking et la restauration d'état.

struct ContentView: View {
    @State private var path = NavigationPath()

    var body: some View {
        NavigationStack(path: $path) {
            List(0..<10) { i in
                NavigationLink("Item \(i)", value: i)
            }
            .navigationTitle("Accueil")
            .navigationDestination(for: Int.self) { value in
                DetailView(index: value)
            }
        }
    }
}

Pour les écrans modaux, on utilise .sheet(isPresented:) (modal carte iOS 13+), .fullScreenCover (plein écran sans drag dismiss), .alert et .confirmationDialog pour les confirmations natives système.

4. Animations déclaratives

Animer en SwiftUI consiste à modifier un état dans un bloc withAnimation { }. Le framework interpole automatiquement les valeurs animables (opacity, scale, offset, color, cornerRadius...) entre les deux états.

struct LikeButton: View {
    @State private var liked = false

    var body: some View {
        Image(systemName: liked ? "heart.fill" : "heart")
            .foregroundStyle(liked ? .red : .gray)
            .scaleEffect(liked ? 1.3 : 1.0)
            .animation(.spring(response: 0.4, dampingFraction: 0.6), value: liked)
            .onTapGesture { liked.toggle() }
    }
}

Pour les transitions plus avancées (effet "hero" entre deux écrans), matchedGeometryEffect identifie une vue par un id et un namespace partagé : SwiftUI interpole position, taille et forme automatiquement entre les états.

5. SwiftUI vs UIKit — quand mixer ?

CritèreUIKit (depuis 2007)SwiftUI (depuis 2019)
ParadigmeImpératif (MVC)Déclaratif (state-driven)
Cycle de vieviewDidLoad, viewWillAppear....onAppear, .task
LayoutAuto Layout + constraintsVStack/HStack/ZStack + frame
Cible minimumiOS 2.0iOS 13 (mais iOS 16+ pour confort)
Live PreviewNonOui (Canvas Xcode)
Camera, AR, MapKitNativeWrap via UIViewRepresentable

✏️ Cas pratique : ContentView complet avec @State et NavigationStack

struct Task: Identifiable {
    let id = UUID()
    var title: String
    var done: Bool = false
}

struct TaskListView: View {
    @State private var tasks: [Task] = [
        Task(title: "Lire la doc SwiftUI"),
        Task(title: "Configurer Xcode 15"),
        Task(title: "Publier sur TestFlight")
    ]
    @State private var newTitle: String = ""

    var body: some View {
        NavigationStack {
            VStack {
                HStack {
                    TextField("Nouvelle tâche", text: $newTitle)
                        .textFieldStyle(.roundedBorder)
                    Button("Ajouter") {
                        guard !newTitle.isEmpty else { return }
                        withAnimation {
                            tasks.append(Task(title: newTitle))
                            newTitle = ""
                        }
                    }
                }.padding()

                List($tasks) { $task in
                    HStack {
                        Image(systemName: task.done ? "checkmark.circle.fill" : "circle")
                            .foregroundStyle(task.done ? .green : .gray)
                            .onTapGesture {
                                withAnimation { task.done.toggle() }
                            }
                        Text(task.title)
                            .strikethrough(task.done)
                    }
                }
            }
            .navigationTitle("Mes tâches")
        }
    }
}
💡 Pro tip Previews : utilise #Preview (macro Swift 5.9+) avec plusieurs configurations pour tester light/dark mode et différentes tailles d'écran sans recompiler.
⚠️ Erreur typique : utiliser @ObservedObject à la place de @StateObject dans la View qui crée le ViewModel provoque une recréation à chaque redraw — perte d'état garantie. Règle : @StateObject dans la View propriétaire, @ObservedObject dans les enfants.

6. Pour aller plus loin

Approfondissement technique

Le passage de @ObservableObject au macro @Observable (iOS 17+) est l'évolution la plus importante de SwiftUI depuis sa naissance. Sous le capot, @Observable applique du tracking par observation granulaire au niveau propriété : SwiftUI ne réinvalide une vue que si la propriété spécifique lue dans body change. Avec l'ancien @Published, toute modification de n'importe quelle propriété déclenchait une réinvalidation de toutes les vues observant l'objet. Le gain de performances est mesurable : sur des écrans complexes (200+ vues), on observe 30 à 60 % de moins de recalculs de body.

La règle d'or : @State pour la propriété d'une vue, @Bindable pour passer un objet observable mutable à une sous-vue, @Environment(\.modelContext) ou similaire pour les services injectés via l'environnement. Le triplet @StateObject/@ObservedObject/@EnvironmentObject reste valide pour la compatibilité descendante mais devient legacy. Une migration partielle est possible : vous pouvez avoir des @Observable et des ObservableObject cohabitant dans la même app.

Sous-section 1 — Custom ViewModifiers et le pattern ViewBuilder

Tout modifier en SwiftUI est en réalité une fonction qui prend une View et retourne une autre View. Pour créer le vôtre, on conforme un type à ViewModifier et on implémente body(content:). L'astuce est d'envelopper l'usage dans une extension View pour bénéficier d'une API fluide. Le bénéfice n'est pas qu'esthétique : un modifier custom est cacheable par le compilateur car il a une identité de type stable, ce qui permet à SwiftUI de mémoriser la sortie. Cela contraste avec un wrapper @ViewBuilder ad-hoc qui doit être reconstruit.

Le @ViewBuilder lui-même mérite attention : c'est un result builder qui transforme une séquence de View en TupleView. Sa limitation officielle est 10 vues (mais on peut combiner avec Group pour contourner). Plus subtil : un if/else dans un @ViewBuilder produit un _ConditionalContent, ce qui change l'identité de la vue selon la branche. SwiftUI détruit puis recrée l'état (@State) à chaque bascule, ce qui peut causer des regressions visuelles. Pour préserver l'état, utiliser un modifier conditionnel : view.opacity(condition ? 1 : 0) au lieu de if condition { view }.

Sous-section 2 — Drag & drop, Transferable et interopérabilité

Le protocole Transferable (iOS 16+) unifie tout transfert de données : drag & drop, copy-paste, partage ShareLink. On implémente static var transferRepresentation: some TransferRepresentation avec un DataRepresentation, FileRepresentation, ou CodableRepresentation. Le runtime sélectionne le format optimal selon le destinataire : si vous draggez un type vers Finder, c'est le FileRepresentation qui est utilisé ; vers une autre app SwiftUI, c'est la version Codable typée.

Combiner .draggable et .dropDestination permet de construire une UI de réordonnancement quasi gratuite. Sur iPad multitâche, .draggable permet le drag inter-app sans code supplémentaire. Le piège : sur iOS, contrairement à macOS, un drag long-press d'un seul élément n'est pas le geste natif. Il faut typiquement initier le drag depuis une zone tactile dédiée ou utiliser EditMode.

Cas pratiques détaillés

✏️ Cas 1 — Liste réordonnable avec drag & drop typé

Scénario : une todo-list où l'utilisateur réordonne les items par drag. Le challenge est de typer le payload (TodoItem) tout en supportant l'export vers d'autres apps comme du texte brut. Transferable avec multiple représentations résout cela élégamment.

struct TodoItem: Codable, Identifiable, Transferable {
    let id: UUID
    var title: String
    var done: Bool

    static var transferRepresentation: some TransferRepresentation {
        // Format natif (drag intra-app, typé)
        CodableRepresentation(contentType: .todoItem)
        // Format texte (export Notes, Mail, etc.)
        ProxyRepresentation(exporting: \.title)
    }
}
extension UTType {
    static let todoItem = UTType(exportedAs: "com.itag.lms.todoItem")
}

struct TodoList: View {
    @State private var items: [TodoItem] = TodoItem.sample
    var body: some View {
        List {
            ForEach(items) { item in
                Text(item.title)
                    .draggable(item)
            }
            .onMove { from, to in items.move(fromOffsets: from, toOffset: to) }
        }
        .dropDestination(for: TodoItem.self) { incoming, _ in
            items.append(contentsOf: incoming); return true
        }
    }
}

✏️ Cas 2 — Modifier custom de "shake animation" pour erreur de formulaire

Scénario : signaler une erreur de validation sur un champ de saisie avec un effet de secousse horizontale (UX classique iOS). Au lieu de réimplémenter à chaque écran, on en fait un ViewModifier réutilisable, drivé par un boolean.

struct ShakeEffect: GeometryEffect {
    var animatableData: CGFloat
    func effectValue(size: CGSize) -> ProjectionTransform {
        let amount: CGFloat = 8
        let dx = amount * sin(animatableData * .pi * 4)
        return ProjectionTransform(CGAffineTransform(translationX: dx, y: 0))
    }
}
extension View {
    func shake(_ trigger: Bool) -> some View {
        modifier(ShakeEffect(animatableData: trigger ? 1 : 0))
            .animation(.linear(duration: 0.4), value: trigger)
    }
}
// Usage
TextField("Email", text: $email)
    .shake(emailInvalid)

Comparaison et benchmark

ApprochePerformanceLisibilitéCas d'usage
@Observable macro (iOS 17+)Excellente (tracking granulaire)ExcellenteTout nouveau code iOS 17+
@ObservableObject + @PublishedMoyenne (réinvalide tout)BonneCompatibilité iOS <17
@State primitifMaximaleExcellenteÉtat local d'une vue, value type
UIViewRepresentableVariable (bridge cost)MoyenneIntégrer UIKit dans SwiftUI
Canvas + TimelineViewExcellente (drawing direct)BonneAnimations 60-120 fps custom
ListBonne (lazy)ExcellenteListes standards iOS
LazyVStack in ScrollViewTrès bonne mais layout customBonneListes avec headers sticky, mix de types
NavigationStack (iOS 16+)Excellente (path typé)ExcellenteNavigation moderne, deep links

Pièges fréquents en production

  • Identity instability dans ForEach — Si l'id change entre deux renders, SwiftUI détruit l'instance et recrée tous les @State. Toujours conformer à Identifiable avec un id stable (UUID, primary key DB). N'utilisez jamais id: \.self sur des structs mutables.
  • Réinvalidation cascade via Environment — Modifier une valeur .environment(\.foo) haut dans la hiérarchie réinvalide tout le sous-arbre. Limiter la portée en injectant l'environnement le plus près possible des consommateurs.
  • List + .id() force scroll reset — Appeler .id(value) sur une List change son identité et fait remonter le scroll en haut à chaque changement de value. Bug classique de "ma liste remonte toute seule".
  • GeometryReader prend toute la place — Contrairement aux autres vues, GeometryReader consomme tout l'espace proposé par défaut, brisant les layouts compacts. Préférer le modifier .background(GeometryReader { ... }) pour mesurer sans perturber.
  • Animations qui sautent au premier render.animation(.default, value: x) anime même la valeur initiale si x change avant l'apparition. Utiliser .transaction { $0.animation = nil } sur le premier render pour figer.
  • Dynamic Type qui casse le layout — Les utilisateurs accessibilité utilisent des tailles XXXL. Tester avec Self._printChanges() + .dynamicTypeSize(.accessibility5) dans le preview. Préférer ScrollView + VStack à HStack pour les formes longues.
  • VoiceOver labels manquants — Une icône sans label est annoncée "image" par VoiceOver. Toujours ajouter .accessibilityLabel("Fermer") sur les boutons-icônes. .accessibilityHidden(true) pour les éléments décoratifs.
  • Preview qui mensonge — Les previews Xcode ne reflètent pas toujours l'app réelle : différences de runtime, ressources d'environnement, dark mode partiel. Toujours valider sur simulateur et device physique avant de livrer.

Outils et écosystème

  • SwiftUI Inspector (Xcode) — Outil intégré Debug → View Debugging → Capture View Hierarchy. Affiche l'arbre des vues SwiftUI au runtime, permet de mesurer les bounds et les réinvalidations.
  • Pow — Librairie de transitions et effets visuels iOS 16+ (Movin/Boost/Counter). Cousine de .transition() mais avec des effets très polishés. movingparts.io/pow
  • SwiftUI Introspect — Permet d'accéder à l'UIView/NSView sous-jacente à un composant SwiftUI pour configurer ce qui n'est pas exposé. Utile en attendant que Apple comble les gaps API. github.com/siteline/swiftui-introspect
  • The Composable Architecture (TCA) — Architecture state-management basée sur Redux, créée par Point-Free. Très utilisée pour les apps SwiftUI complexes. github.com/pointfreeco/swift-composable-architecture
  • Accessibility Inspector — Outil Xcode (Open Developer Tool → Accessibility Inspector) qui audite votre interface VoiceOver, Dynamic Type, contraste. Critique avant submission App Store.
  • SF Symbols app — Browser officiel des 5 000+ symboles système, avec aperçu des poids, variantes et animations. developer.apple.com/sf-symbols
  • SnapshotTesting (Point-Free) — Tests de régression visuelle : prend un snapshot PNG d'une vue et compare aux baselines. github.com/pointfreeco/swift-snapshot-testing

Citations sources officielles

"The Observation framework provides a Swift-specific implementation of the observer design pattern. Use the @Observable() macro to track changes to your data without the overhead of @Published." — developer.apple.com/documentation/observation
"SwiftUI applies accessibility modifiers to your views by inferring them from the views themselves. You can override or supplement these inferred properties to make your app more accessible." — developer.apple.com/documentation/swiftui/accessibility-fundamentals

Glossaire (10 termes clés)

TermeDéfinition
@ObservableMacro Swift 5.9 (iOS 17+) qui rend un type observable avec tracking granulaire par propriété. Remplace @ObservableObject+@Published.
@BindableWrapper qui permet de créer un Binding vers une propriété d'un objet @Observable non possédé par la vue.
ViewModifierProtocole pour créer des modifiers réutilisables. Méthode body(content: Content) -> some View.
ViewBuilderResult builder qui assemble des View en TupleView. Permet la syntaxe déclarative imbriquée.
EnvironmentValuesDictionnaire typé propagé dans l'arbre des vues. Étendu via extension EnvironmentValues + @Entry.
IdentityConcept clé SwiftUI : SwiftUI compare les identités entre renders pour décider de réutiliser ou recréer les vues. Stable = pas de recréation.
TransferableProtocole iOS 16+ unifiant transfert de données (drag&drop, paste, share). Remplace NSItemProvider.
NavigationStackContainer de navigation moderne (iOS 16+) supportant un path typé pour deep linking et state restoration.
Dynamic TypeSystème iOS d'ajustement automatique de la taille de texte selon préférences utilisateur (5 tailles standard + 7 accessibilité).
VoiceOverLecteur d'écran iOS pour utilisateurs malvoyants. SwiftUI infère beaucoup d'annotations ; le développeur complète avec .accessibility*.

Ressources d'approfondissement

Continuez le parcours 🚀

Inscrivez-vous pour accéder aux 5 autres leçons + le quiz final.

Créer mon compte
🍪 Nous utilisons des cookies essentiels et, avec ton accord, des cookies analytiques. En savoir plus

⚙️ Préférences cookies

Choisis quels cookies tu acceptes — modifiable à tout moment.

🔐 Essentiels (obligatoires)Authentification, session, sécurité. Toujours actifs.
📊 Analytics anonymesMesure d'audience anonymisée — aucune donnée personnelle.
📣 MarketingPublicités ITAG pertinentes sur d'autres sites.
💬 Contactez-nous sur WhatsApp