Introduction à la programmation asynchrone en Swift
\n\nLes applications mobiles modernes ne se passent rarement de requêtes réseau, de travail avec une base de données ou de traitement de fichiers volumineux. Toutes ces opérations prennent du temps, et si elles sont exécutées sur le thread principal, l'interface de l'application se "fige", offrant une mauvaise expérience utilisateur. C'est là que la programmation asynchrone vient à la rescousse.
\n\nAvant l'arrivée de Swift 5.5, les développeurs utilisaient des closures (gestionnaires d'achèvement) ou des bibliothèques tierces (Combine, RxSwift), ce qui conduisait souvent à "l'enfer des callbacks" et à un code difficile à lire. Avec l'introduction du nouveau modèle Swift Concurrency (async/await), l'écriture de code asynchrone est devenue aussi simple et linéaire que le code synchrone.
\n\nDans ce guide, nous allons explorer les concepts clés de la programmation asynchrone en Swift à travers des exemples pratiques. Vous apprendrez à utiliser async/await, Task, Actor et d'autres outils pour créer des applications rapides et réactives pour iOS et macOS.
Concepts fondamentaux de Swift Concurrency
\n\n1. Async et Await : le fondement de l'asynchrone
\n\nLes mots-clés async et await sont au cœur du nouveau modèle. async déclare une fonction comme asynchrone, tandis que await suspend l'exécution jusqu'à l'obtention du résultat, sans bloquer le thread.
Prenons un exemple simple : le chargement de données depuis le réseau.
\n\nimport Foundation\n\n// Déclaration d'une fonction asynchrone\nfunc fetchUserData() async throws -> String {\n let url = URL(string: "https://api.example.com/user/1")!\n let (data, _) = try await URLSession.shared.data(from: url)\n return String(data: data, encoding: .utf8) ?? "Erreur"\n}\n\n// Appel de la fonction asynchrone\nTask {\n do {\n let userData = try await fetchUserData()\n print("Données utilisateur : \\(userData)")\n } catch {\n print("Erreur de chargement : \\(error.localizedDescription)")\n }\n}\n\nRemarquez : try await signifie que nous attendons un résultat qui peut générer une erreur. Le code ressemble à du code synchrone classique, mais sans bloquer le thread principal.
2. Task : point d'entrée dans le monde asynchrone
\n\nLes fonctions asynchrones ne peuvent être appelées que depuis un autre contexte asynchrone. Pour lancer une opération asynchrone depuis du code synchrone (par exemple, depuis viewDidLoad()), on utilise Task.
class ViewController: UIViewController {\n \n override func viewDidLoad() {\n super.viewDidLoad()\n \n // Lancement d'une tâche asynchrone\n Task {\n let data = await loadData()\n updateUI(with: data)\n }\n }\n \n func loadData() async -> String {\n // Simulation d'un chargement long\n try? await Task.sleep(nanoseconds: 2_000_000_000)\n return "Données chargées"\n }\n \n func updateUI(with data: String) {\n print("Mise à jour de l'UI : \\(data)")\n }\n}\n\nImportant : Task crée une nouvelle tâche asynchrone qui s'exécute sur un thread d'arrière-plan. Pour mettre à jour l'UI, il faut impérativement revenir sur le thread principal — par défaut, await le fait automatiquement.
3. Actor : protection contre les conditions de course
\n\nLorsqu'on travaille avec le multithreading, le problème de condition de course (data race) survient souvent, lorsque plusieurs threads modifient simultanément une même variable. Swift propose une solution élégante : Actor.
Actor est un type référence qui garantit qu'un seul thread peut accéder à son état mutable à un moment donné.
actor Counter {\n private var value = 0\n \n func increment() {\n value += 1\n }\n \n func getValue() -> Int {\n return value\n }\n}\n\n// Utilisation\nlet counter = Counter