Architecture Swift Concurrency : Patterns et bonnes pratiques
Publié le · Mis à jour le · 30 min
Swift Concurrency transforme profondément la façon dont nous architecturons nos applications iOS. Les patterns classiques comme MVVM, Clean Architecture et Repository doivent être repensés pour tirer parti des actors, de l'isolation et de Sendable. Ce guide exhaustif couvre les principes fondamentaux, les patterns d'implémentation, les risques à éviter et les bonnes pratiques pour construire des applications robustes et maintenables.
Principes fondamentaux
Avant de plonger dans les patterns spécifiques à Swift Concurrency, rappelons les principes architecturaux essentiels.
Séparation des responsabilités
Une architecture saine sépare clairement les responsabilités :
| Couche | Responsabilité | Exemples |
|---|---|---|
Présentation | Affichage et interactions utilisateur | View, ViewController, ViewModel |
Domaine | Logique métier | UseCase, Interactor, Entity |
Données | Accès aux données | Repository, DataSource, API Client |
Cette séparation permet de tester chaque couche indépendamment et de modifier une couche sans impacter les autres.
Dependency Inversion Principle
Les couches hautes (domaine) ne dépendent pas des couches basses (données). On utilise des protocoles pour inverser les dépendances :
Unidirectional Data Flow
Le flux de données est unidirectionnel : les actions descendent, les états remontent.
Les couches architecturales avec Swift Concurrency
Couche Présentation : ViewModel
Le ViewModel est le pont entre la View et la logique métier. Avec Swift Concurrency, il doit être isolé au MainActor :
Pourquoi @MainActor sur le ViewModel ?
Le ViewModel reçoit des signaux de la View (isolée au MainActor) et publie des changements vers la View. L'isoler au MainActor :
- Garantit que les mises à jour d'état sont thread-safe
- Élimine les warnings de concurrence
- Permet au ViewModel d'être Sendable automatiquement
- Simplifie le code en évitant les
await MainActor.run
- Simplifie le code en évitant les
Couche Domaine : UseCase / Interactor
Les UseCases encapsulent la logique métier. Ils sont généralement nonisolated (actor-agnostic) et Sendable :
Pourquoi struct et Sendable ?
Les UseCases n'ont pas d'état mutable. Utiliser une struct :
- Les rend automatiquement Sendable si toutes les propriétés le sont
- Encourage l'immuabilité
- Facilite les tests (pas de shared state)
Couche Données : Repository
Le Repository abstrait l'accès aux données. Il peut être implémenté comme un actor si l'état est partagé :
Quand utiliser un actor pour le Repository ?
| Situation | Recommandation |
|---|---|
Pas d'état partagé | struct ou class simple |
Cache en mémoire | actor |
Accès concurrent à une base de données | actor |
Simple wrapper d'API | struct Sendable |
DataSource : API Client
L'API Client gère les appels réseau. Il n'a généralement pas besoin d'être un actor :
Le problème de la réentrance
La réentrance est le piège principal des actors. Comprendre ce problème est crucial pour une architecture robuste.
Qu'est-ce que la réentrance ?
Quand un actor suspend son exécution (await), d'autres tâches peuvent s'exécuter sur cet actor avant que la première ne reprenne :
Solutions à la réentrance
Solution 1 : Valider l'état APRÈS la suspension
Solution 2 : Mutations synchrones uniquement
Solution 3 : Coalescing des requêtes
Pour éviter les appels réseau dupliqués :
Patterns architecturaux avancés
Pattern : Service d'authentification
L'authentification est un cas classique où la réentrance pose problème :
Pattern : Coordinator avec Navigation
Pattern : Repository avec Offline-First
Pattern : State Machine avec Actor
Injection de dépendances
Container de dépendances
Protocol Witness avec Sendable
Pour une approche plus fonctionnelle et testable :
Testabilité
Tester un ViewModel
Tester un Actor
Risques et pièges à éviter
1. Deadlock avec le Cooperative Thread Pool
Ne jamais bloquer un thread du pool coopératif :
2. Oublier @MainActor sur le ViewModel
3. Continuation jamais appelée
4. Ignorer l'annulation
5. Sendable non respecté
Migration depuis GCD/Combine
De GCD à async/await
De Combine à AsyncSequence
Bonnes pratiques récapitulatives
Isolation des couches
| Couche | Isolation recommandée |
|---|---|
View | @MainActor (automatique avec SwiftUI) |
ViewModel | @MainActor |
UseCase | nonisolated (Sendable struct) |
Repository | actor (si état partagé) ou struct Sendable |
DataSource | struct Sendable |
Checklist architecture
ViewModel isolé au @MainActor
UseCases stateless et Sendable
Repositories avec actor si cache/état partagé
Protocoles pour l'injection de dépendances
Gestion de la réentrance dans les actors
Vérification de l'annulation dans les boucles
Pas de travail bloquant sur le cooperative thread pool
Tests unitaires avec mocks Sendable
Pour aller plus loin
Une architecture bien pensée avec Swift Concurrency offre :
- Sécurité : élimination des data races à la compilation
- Maintenabilité : code clair avec flux de données prévisible
- Testabilité : couches isolées et mockables
- Performance : parallélisme efficace sans overhead