Comprendre async/await en Swift (sans être technique)
Depuis iOS 13/15 et les versions récentes de Swift, Apple a introduit une manière beaucoup plus simple
d’écrire du code asynchrone : async/await.
Le but : rendre le code plus lisible, plus facile à maintenir et moins sujet aux bugs.
Même si tu n’es pas développeur, tu peux comprendre l’idée générale :
comment une app gère plusieurs choses en même temps sans bloquer l’écran, et
pourquoi async/await est une grosse amélioration.
1. C’est quoi du “code asynchrone” ?
Une application fait souvent des opérations qui prennent du temps :
- charger des données depuis Internet,
- lire un fichier,
- faire un calcul lourd (par exemple analyser une image).
Si l’app attendait ces opérations en bloquant l’interface, l’utilisateur aurait l’impression que tout est figé, voire planté.
Le code asynchrone, c’est simplement : « Je lance une tâche longue, mais je n’attends pas devant. Je fais autre chose en attendant le résultat. »
Sur mobile, c’est vital : l’UI doit rester fluide pendant que l’app travaille en arrière-plan.
2. Avant async/await : les callbacks et les closures
Avant async/await, en Swift (et dans beaucoup d’autres langages), on utilisait des
callbacks (ou closures) pour gérer l’asynchrone.
Exemple (simplifié) :
func fetchUser(completion: @escaping (User?, Error?) -> Void) {
apiClient.get("/user") { user, error in
completion(user, error)
}
}
Le problème de ce style de code :
- les callbacks s’imbriquent les uns dans les autres,
- le code devient difficile à lire (“pyramide de l’enfer”),
- la gestion des erreurs est compliquée à suivre.
Quand on enchaîne plusieurs appels réseau ou étapes, ça peut vite ressembler à :
func loadData(completion: @escaping (Result<FinalData, Error>) -> Void) {
fetchUser { user, error in
if let user = user {
fetchPosts(for: user) { posts, error in
if let posts = posts {
// Et ainsi de suite...
}
}
}
}
}
Pour un humain, ce n’est pas agréable à lire, à relire, ou à maintenir.
3. L’idée de async/await
Avec async/await, Apple a voulu :
- garder le comportement asynchrone (ne pas bloquer l’UI),
- mais rendre le code aussi lisible qu’un code “normal”.
Idée clé :
async indique qu’une fonction peut être longue et se mettre en pause,
await indique qu’on attend le résultat de cette fonction sans bloquer le reste.
Visuellement, le code ressemble à du code synchrone (un appel après l’autre), mais sous le capot, il est non bloquant.
4. Une première fonction async en Swift
Exemple : récupérer un utilisateur depuis un serveur
struct User: Decodable {
let id: Int
let name: String
}
func fetchUser() async throws -> User {
let url = URL(string: "https://example.com/user")!
let (data, _) = try await URLSession.shared.data(from: url)
let user = try JSONDecoder().decode(User.self, from: data)
return user
}
Points importants :
asyncdans la signature : la fonction est asynchrone.throws: elle peut échouer (erreur réseau, décodage, etc.).try await: on attend la réponse réseau, mais sans bloquer l’UI.- Le code est linéaire, facile à suivre étape par étape.
5. Appeler une fonction async : où mettre await ?
On ne peut utiliser await que depuis une fonction elle-même marquée async
ou depuis un Task.
5.1 Exemple dans une autre fonction async
func loadUserName() async throws -> String {
let user = try await fetchUser()
return user.name
}
Ici :
loadUserNameest async, elle peut doncawaitle résultat defetchUser().- La logique “métier” est lisible : “je récupère l’utilisateur, puis je retourne son nom”.
5.2 Exemple dans une vue SwiftUI
Dans SwiftUI, on ne peut pas marquer une fonction de rendu comme async,
mais on peut lancer une Task depuis un événement.
struct UserView: View {
@State private var name: String = "Chargement..."
@State private var errorMessage: String?
var body: some View {
VStack {
Text(name)
if let errorMessage = errorMessage {
Text(errorMessage)
.foregroundColor(.red)
}
Button("Recharger") {
Task {
await load()
}
}
}
.task {
await load()
}
}
func load() async {
do {
let user = try await fetchUser()
name = user.name
} catch {
errorMessage = "Impossible de charger l'utilisateur"
}
}
}
Explication :
.task { ... }lance automatiquement du code asynchrone quand la vue apparaît.Task { ... }dans le bouton permet de relancer le chargement manuellement.load()estasync, donc peut utilisertry await fetchUser().
6. async/await et gestion des erreurs
Avec async/await, on réutilise la gestion d’erreurs classique de Swift : do / try / catch.
Exemple :
func loadUserSafely() async {
do {
let user = try await fetchUser()
print("Utilisateur : \(user.name)")
} catch {
print("Erreur pendant le chargement : \(error)")
}
}
L’avantage : erreurs réseau, erreurs de parsing, etc. sont traitées de manière uniforme, au même endroit, au lieu de se disperser dans plusieurs callbacks imbriqués.
7. Plusieurs appels async à la suite
Async/await brille vraiment lorsqu’on enchaîne plusieurs opérations.
Avant (callbacks imbriquées) :
fetchUser { user, error in
guard let user = user else { return }
fetchPosts(for: user) { posts, error in
guard let posts = posts else { return }
fetchComments(for: posts) { comments, error in
// etc.
}
}
}
Avec async/await :
func loadUserData() async throws -> (User, [Post], [Comment]) {
let user = try await fetchUser()
let posts = try await fetchPosts(for: user)
let comments = try await fetchComments(for: posts)
return (user, posts, comments)
}
Pour un humain, c’est beaucoup plus lisible et logique :
- Je récupère l’utilisateur.
- Je récupère ses posts.
- Je récupère les commentaires associés.
Tout en restant asynchrone en interne.
8. Lancer des tâches en parallèle
Async/await permet aussi de lancer plusieurs opérations en parallèle, sans écrire de code très compliqué.
Exemple : charger en même temps :
- les informations de l’utilisateur,
- ses posts,
- ses notifications.
func loadDashboard() async throws -> (User, [Post], [Notification]) {
async let user = fetchUser()
async let posts = fetchPosts()
async let notifications = fetchNotifications()
// Les trois appels sont lancés en parallèle.
// await va récupérer leurs résultats quand ils sont prêts.
return try await (user, posts, notifications)
}
Ce code est toujours très lisible, mais profite du parallélisme pour aller plus vite.
9. async/await côté UIKit
Dans une application UIKit “classique” (UIViewController, Storyboard, etc.),
la logique est similaire, mais on va souvent utiliser Task pour lancer du code async
depuis des méthodes qui ne peuvent pas être async (par exemple viewDidLoad()).
class UserViewController: UIViewController {
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var errorLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
Task {
await load()
}
}
func load() async {
do {
let user = try await fetchUser()
nameLabel.text = user.name
errorLabel.text = nil
} catch {
errorLabel.text = "Erreur de chargement"
}
}
}
Idée : on garde les principes :
Task { ... }pour lancer de l’async.- une fonction
asyncpour gérer la logique.
10. Forces et faiblesses de async/await
10.1 Forces
- Lisibilité : le code ressemble à du code “normal”, très linéaire.
- Moins de bugs : moins de callbacks imbriqués, moins de risques d’oublier des cas.
- Meilleure gestion des erreurs : via
try/catch. - Parallélisme plus simple : avec
async letet les tâches. - Intégré à l’écosystème moderne Swift : SwiftUI, URLSession, etc. exposent des APIs async.
10.2 Faiblesses / limites
- Nécessite des versions récentes de Swift et des OS (iOS 15+, macOS récents...).
- Ce n’est pas rétrocompatible avec les vieux systèmes (sans bricolage).
- Demande un changement de mentalité pour les développeurs habitués aux callbacks/promise.
11. Résumé global pour les non techniques
On peut résumer la différence ainsi :
- Avant : beaucoup de fonctions avec “quand tu as fini, appelle-moi avec callback X”. Le code est fragmenté, imbriqué, plus difficile à lire.
- Avec async/await : le code ressemble à une histoire qu’on lit de haut en bas :
“Récupère l’utilisateur, puis ses données, puis affiche-les”, tout en restant non bloquant.
Pour un chef de projet, un product owner ou un profil non technique, comprendre async/await,
c’est comprendre que :
- l’app peut faire plusieurs choses “en même temps” sans bloquer l’écran,
- le code des développeurs est plus simple et donc plus fiable,
- les erreurs réseau (pas de connexion, serveur en panne…) sont mieux gérées.
En pratique, async/await est devenu la façon recommandée d’écrire
du code asynchrone en Swift moderne, aussi bien pour les nouvelles apps que pour la migration
progressive de code existant.