SwiftUI : Construire des interfaces modernes pour iOS 19

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

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 19, lancé à la WWDC 2026, apporte des améliorations majeures : @State est désormais une macro avec initialisation différée, les swipe actions fonctionnent sur n'importe quelle vue, et les nouvelles API de conteneurs réordonnables simplifient le glisser-déposer sur toutes les plateformes.

Pourquoi SwiftUI en 2026 ?

SwiftUI est désormais mature avec iOS 19, offrant le design Liquid Glass qui modernise automatiquement les applications existantes, une initialisation @State optimisée, 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

// Each screen is a struct that implements the View protocol
struct ContentView: View {

    // The "body" property describes what the view displays
    // "some View" means "a type of View, but Swift figures it out automatically"
    var body: some View {

        // VStack = "Vertical Stack": stacks elements vertically
        // spacing: 20 = 20 points of space between each element
        VStack(spacing: 20) {

            // A simple text with style modifiers
            Text("Welcome to SharpSkill")
                .font(.largeTitle)      // Large title font
                .fontWeight(.bold)      // Bold text

            Text("Prepare for your iOS interviews")
                .font(.subheadline)     // Smaller font
                .foregroundColor(.secondary)  // Gray secondary color

            // Button with an action (closure) and a label
            Button("Get Started") {
                print("Button tapped!")
            }
            .buttonStyle(.borderedProminent)  // Filled blue button style
        }
        .padding()  // Adds margins around the 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
// Example 1: padding THEN background
Text("SwiftUI")
    .padding()           // 1. Adds 16pt of space around the text
    .background(.blue)   // 2. Blue background covers text + padding
    .foregroundColor(.white)
// Result: white text on blue rectangle with margins

// Example 2: background THEN padding (reversed order!)
Text("SwiftUI")
    .background(.blue)   // 1. Tight blue background around text only
    .padding()           // 2. Transparent padding around blue background
    .foregroundColor(.white)
// Result: white text on small blue rectangle, surrounded by empty space

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 {
        // Main HStack: photo on left, info on right
        HStack(spacing: 16) {

            // ZStack to overlay the badge on the photo
            ZStack(alignment: .bottomTrailing) {
                // Profile image (circle)
                Image("avatar")
                    .resizable()
                    .frame(width: 60, height: 60)
                    .clipShape(Circle())

                // Green "verified" badge at bottom right
                Image(systemName: "checkmark.circle.fill")
                    .foregroundColor(.green)
                    .background(Circle().fill(.white))  // White circle behind
            }

            // VStack to stack name and role vertically
            VStack(alignment: .leading, spacing: 4) {
                Text("Marie Dupont")
                    .font(.headline)

                Text("iOS Developer")
                    .font(.subheadline)
                    .foregroundColor(.secondary)
            }

            // Spacer pushes everything to the left
            Spacer()

            // Chevron on right indicates it's tappable
            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 recalcule 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.

Mise à jour iOS 19 : @State est désormais une macro avec initialisation différée. Dans les versions précédentes, une nouvelle instance de classe pouvait être créée à chaque initialisation d'une vue. Maintenant, SwiftUI crée l'objet uniquement au premier accès, éliminant les créations d'objets superflues. Ce changement est rétro-porté vers iOS 17, les applications existantes en bénéficient automatiquement.

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

CounterView.swiftswift
struct CounterView: View {
    // @State creates a "source of truth" for this view
    // private because state shouldn't be modified from outside
    @State private var count = 0

    var body: some View {
        VStack(spacing: 30) {
            // This Text updates automatically when count changes
            Text("\(count)")
                .font(.system(size: 72, weight: .bold))

            HStack(spacing: 40) {
                // Decrement button
                Button(action: {
                    count -= 1  // Modifies state → view refreshes
                }) {
                    Image(systemName: "minus.circle.fill")
                        .font(.largeTitle)
                }

                // Increment button
                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 réexécute body pour mettre à jour l'affichage. Vous n'avez pas à gérer manuellement la mise à jour du label.

@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
// Parent view: owns the state
struct SignupForm: View {
    @State private var username = ""       // Source of truth
    @State private var isValid = false     // Validation state

    var body: some View {
        VStack(spacing: 20) {
            // Pass BINDINGS (with $) to the child view
            UsernameField(username: $username, isValid: $isValid)

            Button("Create Account") {
                // Submit the form
            }
            .disabled(!isValid)  // Disabled if invalid
            .buttonStyle(.borderedProminent)
        }
        .padding()
    }
}

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

swift
// Child view: receives and modifies state via @Binding
struct UsernameField: View {
    @Binding var username: String   // Connection to parent's @State
    @Binding var isValid: Bool

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            TextField("Username", text: $username)
                .textFieldStyle(.roundedBorder)
                .onChange(of: username) { oldValue, newValue in
                    // Validation: at least 3 characters
                    isValid = newValue.count >= 3
                }

            // Visual feedback
            HStack {
                Image(systemName: isValid ? "checkmark.circle" : "xmark.circle")
                Text("Minimum 3 characters")
            }
            .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. Pour une comparaison approfondie, consultez SwiftUI @Observable vs @State : quand utiliser quoi.

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 lets SwiftUI track each element
struct Interview: Identifiable {
    let id = UUID()           // Auto-generated unique identifier
    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 {
    // Data to display (in reality, this would come from an API)
    @State private var interviews = [
        Interview(technology: "iOS", difficulty: "Intermediate", questionCount: 25),
        Interview(technology: "Android", difficulty: "Advanced", questionCount: 30),
        Interview(technology: "React", difficulty: "Beginner", questionCount: 20)
    ]

    var body: some View {
        // NavigationStack enables the navigation bar
        NavigationStack {
            List {
                // ForEach iterates over each interview
                // Thanks to Identifiable, no need to specify id:
                ForEach(interviews) { interview in
                    InterviewRow(interview: interview)
                }
            }
            .navigationTitle("My Interviews")
        }
    }
}

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 {
            // Left column: title and subtitle
            VStack(alignment: .leading, spacing: 4) {
                Text(interview.technology)
                    .font(.headline)

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

            Spacer()

            // Badge with question count
            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 avec iOS 19

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 (enhanced version)swift
struct InterviewListView: View {
    @State private var interviews = [/* ... data ... */]

    var body: some View {
        NavigationStack {
            List {
                ForEach(interviews) { interview in
                    InterviewRow(interview: interview)
                }
                // Swipe to delete
                .onDelete(perform: deleteInterview)
                // Drag and drop to reorder
                .onMove(perform: moveInterview)
            }
            .navigationTitle("My Interviews")
            .toolbar {
                // "Edit" button that activates edit mode
                EditButton()
            }
        }
    }

    // Deletes elements at specified indices
    private func deleteInterview(at offsets: IndexSet) {
        interviews.remove(atOffsets: offsets)
    }

    // Moves elements from one position to another
    private func moveInterview(from source: IndexSet, to destination: Int) {
        interviews.move(fromOffsets: source, toOffset: destination)
    }
}

iOS 19 introduit les conteneurs réordonnables : le nouveau modificateur .reorderable() combiné à reorderContainer(for:) permet de réordonner les éléments dans List, LazyVGrid et les layouts personnalisés avec des animations intégrées. Le système calcule automatiquement les différences via un paramètre closure. Cela fonctionne aussi sur watchOS, une première pour la montre connectée d'Apple.

De plus, les swipe actions fonctionnent désormais sur n'importe quelle vue. Auparavant limitées à List, le modificateur swipeActions associé au nouveau swipeActionsContainer() fonctionne sur ScrollView, les lazy stacks et les layouts personnalisés.

Pour plus de détails sur les patterns de navigation, consultez SwiftUI NavigationStack : questions d'entretien.

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)
                // Dimensions change based on state
                .frame(
                    width: isExpanded ? 300 : 150,
                    height: isExpanded ? 200 : 100
                )

            Button(isExpanded ? "Collapse" : "Expand") {
                // withAnimation animates ALL visual changes
                // resulting from this state change
                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. Vous n'avez pas à spécifier quoi animer.

Transitions : animer l'apparition et la 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("Show Details") {
                withAnimation(.easeInOut(duration: 0.3)) {
                    showDetails.toggle()
                }
            }

            // This view appears/disappears with a transition
            if showDetails {
                DetailCard()
                    // Asymmetric transition: different for entry and exit
                    .transition(
                        .asymmetric(
                            insertion: .scale.combined(with: .opacity),  // Entry: zoom + fade
                            removal: .slide                               // Exit: slide
                        )
                    )
            }
        }
    }
}

struct DetailCard: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Interview Details")
                .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 {
                // State: loading in progress
                ProgressView("Loading...")

            } else if let error = errorMessage {
                // State: error
                VStack(spacing: 16) {
                    Image(systemName: "exclamationmark.triangle")
                        .font(.largeTitle)
                        .foregroundColor(.orange)
                    Text(error)
                    Button("Retry") {
                        Task { await loadQuestions() }
                    }
                }

            } else {
                // State: success, display data
                List(questions) { question in
                    Text(question.title)
                }
            }
        }
        // .task runs automatically when the view appears
        .task {
            await loadQuestions()
        }
        // Pull-to-refresh
        .refreshable {
            await loadQuestions()
        }
    }

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

        do {
            // Async API call
            questions = try await QuestionService.shared.fetchQuestions()
        } catch {
            errorMessage = "Unable to load 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. Pour plus de détails sur les patterns async en Swift, consultez Questions d'entretien Swift Structured Concurrency.

Sources

Ce qu'il faut retenir de SwiftUI sur iOS 19

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

  • 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, désormais avec initialisation différée
  • Stacks : combinez VStack, HStack et ZStack pour des layouts flexibles
  • List : affichez des collections avec peu de code et des interactions natives
  • Conteneurs réordonnables : iOS 19 ajoute .reorderable() pour le glisser-déposer dans n'importe quel conteneur
  • Swipe actions partout : plus limitées à List, elles fonctionnent sur ScrollView et les layouts personnalisés
  • Animations : utilisez withAnimation pour des transitions fluides automatiques
  • Async/await : chargez vos données avec .task et gérez les états loading/error

Checklist préparation entretien

  • 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 (suppression, déplacement, iOS 19 reorderable)
  • 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.

Défi du jour

Tu saurais repérer le bug en iOS ?

Un vrai bout de code, un bug caché, une tentative par jour. Sans compte pour essayer.

Anthony Fillion-Maillet

Écrit par

Anthony Fillion-Maillet

Fondateur de SharpSkill

Développeur fullstack depuis plus de 10 ans. Il dirige SharpSkill et répond de tout ce qui y est publié.

Mis à jour le 20 août 2026

Tags

#swiftui
#ios
#swift
#ui
#apple

Partager

Articles similaires