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.

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.
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 :
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 :
// 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 spaceLa 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.
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.
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 :
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.
// 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 :
// 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.
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 :
// 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 :
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é :
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 :
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.
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 :
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.
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.
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
- What's New in SwiftUI - WWDC 2026 - Vue d'ensemble des changements SwiftUI iOS 19
- Documentation SwiftUI @State - Référence officielle de la macro @State et de l'initialisation différée
- Swift 6.3 Release Notes - Améliorations du langage qui supportent les gains de performance SwiftUI
- SwiftUI in WWDC 2026 - Analyse détaillée des nouvelles API
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
withAnimationpour des transitions fluides automatiques - Async/await : chargez vos données avec
.tasket 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.
Tu saurais repérer le bug en iOS ?
Un vrai bout de code, un bug caché, une tentative par jour. Sans compte pour essayer.

Écrit par
Anthony Fillion-MailletFondateur 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
Partager
Articles similaires

SwiftUI Performance : optimiser LazyVStack et listes complexes
Techniques d'optimisation pour LazyVStack et listes SwiftUI. Réduire la consommation mémoire, améliorer le scrolling et éviter les pièges de performance courants.

SwiftUI Custom ViewModifiers : patterns réutilisables pour design system
Créer des ViewModifiers SwiftUI personnalisés pour un design system cohérent. Patterns, bonnes pratiques et exemples concrets pour styliser vos vues iOS efficacement.

SwiftUI @Observable vs @State : quand utiliser quoi en 2026
Comprendre les différences entre @Observable et @State en SwiftUI pour choisir le bon outil de gestion d'état selon le contexte de votre application iOS.