SCours SwiftUI
Fiche 07.01

Fiche 07.01 — async/await, Task, .task et @MainActor

Objectif

Comprendre la concurrence moderne en Swift pour charger des données sans bloquer l’interface. C’est indispensable pour les appels réseau, Firebase, fichiers, images et traitements longs.

1. Le problème

Une app ne doit jamais bloquer l’interface pendant un chargement.

Mauvais principe :

Swift
// Bloque le thread principal si le traitement est lourd let data = loadDataSynchronously()

Bon principe :

Swift
let data = try await loadData()

await indique que le code attend un résultat asynchrone sans bloquer inutilement l’app.

2. Fonction async

Swift
func fetchUser() async throws -> User { // appel réseau ou Firebase User(id: "1", name: "Guillaume") }

Une fonction async peut être suspendue pendant son exécution.

Une fonction throws peut lancer une erreur.

On l’appelle avec :

Swift
let user = try await fetchUser()

3. Task dans une action utilisateur

Dans un bouton SwiftUI, on ne peut pas directement faire await dans une closure classique. On utilise Task.

Swift
Button("Charger") { Task { await viewModel.loadData() } }

Task lance un contexte asynchrone.

4. .task sur une vue

.task lance une action asynchrone quand la vue apparaît.

Swift
struct UserView: View { @StateObject private var viewModel = UserViewModel() var body: some View { content .task { await viewModel.loadUser() } } @ViewBuilder private var content: some View { if viewModel.isLoading { ProgressView() } else { Text(viewModel.userName) } } }

.task est souvent préférable à onAppear pour du code async.

5. @MainActor

Les mises à jour de l’interface doivent se faire sur le thread principal. Avec SwiftUI, on marque souvent le ViewModel en @MainActor.

Swift
@MainActor final class UserViewModel: ObservableObject { @Published var isLoading = false @Published var userName = "" func loadUser() async { isLoading = true defer { isLoading = false } do { try await Task.sleep(nanoseconds: 500_000_000) userName = "Guillaume" } catch { userName = "Erreur" } } }

@MainActor simplifie la sécurité des mises à jour UI.

6. do/catch

Swift
func loadUser() async { isLoading = true errorMessage = nil do { let user = try await service.fetchUser() self.user = user } catch { errorMessage = "Impossible de charger l’utilisateur." } isLoading = false }

do/catch est indispensable pour les appels réseau ou services qui peuvent échouer.

7. Cancellation simple

Une tâche peut être annulée, par exemple si l’utilisateur quitte l’écran.

Swift
func search() async { do { try Task.checkCancellation() let results = try await service.search() self.results = results } catch is CancellationError { // L’utilisateur a quitté ou la recherche a été annulée } catch { errorMessage = "Erreur de recherche" } }

Tu n’as pas besoin de maîtriser tous les détails au début, mais il faut savoir qu’une tâche peut être annulée.

8. Pull to refresh

Swift
struct FeedView: View { @StateObject private var viewModel = FeedViewModel() var body: some View { List(viewModel.items) { item in Text(item.title) } .task { await viewModel.load() } .refreshable { await viewModel.load() } } }

refreshable fonctionne naturellement avec async/await.

Résumé

  • async définit une fonction asynchrone.
  • await attend son résultat.
  • Task lance du code async depuis une action.
  • .task lance du code async quand une vue apparaît.
  • @MainActor protège les mises à jour UI.
  • do/catch gère les erreurs.
  • refreshable permet un pull-to-refresh async simple.