Architecture Swift Concurrency : Patterns et bonnes pratiques

Publié le · Mis à jour le · 30 min

Wlad
Wlad
Fondateur & Tech Lead Swift

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 :

CoucheResponsabilité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

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 ?

SituationRecommandation

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

CoucheIsolation 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

Ressources officielles