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.

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.
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 :
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 :
// 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 videLa 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 {
// 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.
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 :
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.
// 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 :
// 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.
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 :
// 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 :
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é :
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 :
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.
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 :
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.
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 {
// É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
withAnimationpour des transitions fluides automatiques - Async/await : chargez vos données avec
.tasket 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
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.