Fiche 07.07 — Concurrence : annulation, async let et TaskGroup
Objectif
Maîtriser l'annulation coopérative d'une Task, le parallélisme structuré avec async let et withTaskGroup, et le pattern search-as-you-type (debounce + annulation). C'est exactement ce qui sépare un code async qui "marche" d'un code async qui ne fuit pas et ne fait pas tourner le CPU pour rien.
1. Stocker une Task et l'annuler
En UIKit tu gardais une référence sur ton URLSessionDataTask (ou un DispatchWorkItem) pour pouvoir appeler .cancel(). Ici c'est pareil : tu stockes la Task, et tu l'annules.
Point clé : cancel() ne tue rien de force. L'annulation est coopérative. Elle se contente de positionner un flag. C'est ton code (ou les API système comme URLSession) qui doit le vérifier.
2. Vérifier l'annulation : isCancelled vs checkCancellation()
Deux façons de réagir au flag, selon que tu veux gérer toi-même ou lancer une erreur.
Réflexe à avoir : dans une boucle longue ou avant une opération coûteuse, place un try Task.checkCancellation(). Sans ça, ta Task annulée continue de bosser jusqu'au bout — l'annulation est silencieusement ignorée.
Bon à savoir : la plupart des API async système coopèrent déjà. try await URLSession.shared.data(from:) lance URLError(.cancelled) (ou propage l'annulation) si la Task est annulée, et Task.sleep(...) lance CancellationError. Tu n'as donc pas à vérifier manuellement entre chaque await système.
3. .task : auto-annulé au démontage de la vue
C'est LA différence à connaître en entretien. Un Task { } lancé dans un bouton ou onAppear n'est rattaché à rien : si la vue disparaît, la Task continue.
.task lie le cycle de vie de la Task à celui de la vue. Quand SwiftUI retire la vue, il appelle cancel() pour toi — d'où l'importance des points 1 et 2 pour que ça serve à quelque chose.
4. .task(id:) : relance/annule quand l'id change
Quand un paramètre change (un userID sélectionné, un filtre), tu veux annuler le chargement en cours et en relancer un nouveau. .task(id:) fait exactement ça automatiquement.
En UIKit tu aurais dû : annuler manuellement le dataTask du user précédent, en créer un nouveau, gérer le cas où l'ancienne réponse arrive après la nouvelle (race condition / réponses dans le désordre). Ici .task(id:) élimine tout ça gratuitement.
5. Search-as-you-type : debounce + annulation
Le cas réel par excellence. On combine les pièces : .task(id: query) annule la recherche précédente dès que le texte change, et Task.sleep sert de debounce — si l'utilisateur tape vite, l'ancienne Task est annulée pendant son sleep et ne part jamais en réseau.
Erreur fréquente : faire le debounce avec un Timer / DispatchQueue.asyncAfter à la main. Inutile ici — l'annulation de .task(id:) + Task.sleep couvre debounce ET annulation de la requête en cours d'un seul geste.
6. Parallélisme : async let
Trois appels indépendants. En séquentiel, tu attends la somme des durées. Avec async let, ils partent en parallèle et tu await les trois ensemble.
À retenir : async let lance la tâche immédiatement à la ligne de déclaration. Le await ne fait que récupérer le résultat. Si une des branches throw, les autres sont automatiquement annulées (parallélisme structuré). C'est le bon outil quand tu connais le nombre fixe de tâches à l'avance.
7. withTaskGroup : N tâches dynamiques + agrégation
Quand le nombre de tâches dépend des données (télécharger les vignettes de N images, fetcher M endpoints d'une liste), async let ne suffit plus. Utilise un task group.
Points utiles :
- Les
addTaskpartent en parallèle ;for try await ... in groupconsomme les résultats dans l'ordre d'arrivée, pas l'ordre d'ajout. - Si le bloc throw (ou si la Task parente est annulée), le groupe annule toutes les tâches enfants restantes.
- Variante
withTaskGroup(sansThrowing) quand les enfants ne throw pas. Pour limiter le nombre de tâches concurrentes, ne fais pas toutaddTaskd'un coup : utilise un pattern "fenêtre glissante" (ajoute une nouvelle tâche chaque fois qu'une se termine).
Points à connaître
cancel()ne tue pas la Task. Sanstry Task.checkCancellation()/Task.isCancelleddans tes boucles, l'annulation est ignorée et le travail continue.CancellationErrorn'est pas un vrai échec. Catch-le séparément ; ne montre pas une alerte d'erreur à l'utilisateur quand il a juste retapé dans la barre de recherche.Task { }≠.task. Le premier survit au démontage de la vue (fuite, écriture dans un modèle mort) ; le second est auto-annulé. Préfère toujours.task/.task(id:).async letlance tout de suite, à la déclaration — pas auawait. Si tu mets tesasync letaprès une longue opération, tu perds le parallélisme.
Exercice
Construis un écran de recherche d'utilisateurs annulable. Une @Observable SearchModel avec query et results, une fonction search(_:) qui Task.sleep(for: .milliseconds(300)) (debounce) puis appelle une API factice try await fakeSearch(text). Dans la vue, branche .searchable(text:) + .task(id: model.query). Vérifie en tapant vite que les requêtes intermédiaires ne s'exécutent jamais (ajoute un print après le sleep).
Question d'entretien
« Comment annules-tu une requête réseau en Swift Concurrency ? »
Je stocke la Task dans une propriété et j'appelle task?.cancel(). L'annulation est coopérative : URLSession/Task.sleep la respectent nativement, et dans mon propre code je place try Task.checkCancellation() ou je teste Task.isCancelled dans les boucles. Dans une vue SwiftUI, j'utilise .task (auto-annulé au démontage) ou .task(id:) (annule + relance quand l'id change).
« async let vs withTaskGroup, quand utiliser quoi ? »
async let quand j'ai un nombre fixe et connu de tâches hétérogènes à paralléliser (profil + feed + stats), avec un await final qui combine les résultats. withTaskGroup quand le nombre de tâches est dynamique (boucle sur N éléments) et homogène, avec agrégation des résultats au fur et à mesure. Les deux sont du parallélisme structuré : annulation et erreurs se propagent automatiquement aux enfants.
« Quelle est la différence entre Task { } et le modifier .task ? »
Task { } crée une tâche détachée du cycle de vie de la vue : elle continue même après le démontage. .task lie la tâche à la vue et l'annule automatiquement quand la vue disparaît — c'est le défaut sûr pour éviter les fuites et les écritures dans un état obsolète.
Résumé
- Stocke la
Taskdans une propriété pour pouvoir l'cancel(); l'annulation est coopérative. try Task.checkCancellation()(throw) ouTask.isCancelled(manuel) dans les boucles/avant les opérations coûteuses..taskest auto-annulé au démontage ;.task(id:)annule + relance quand l'id change.- Search-as-you-type =
.task(id: query)+Task.sleep(debounce + annulation de la recherche précédente en un seul pattern). async let: N fixe de tâches parallèles, combinées auawaitfinal.withTaskGroup: N dynamique de tâches parallèles, résultats agrégés au fil de l'eau.- Pont UIKit : remplace
urlSessionTask.cancel()/dispatchWorkItem.cancel()partask?.cancel(), avec en bonus l'annulation structurée et automatique.