@MainActor — qu'est-ce que c'est, application et particularités dans le code asynchrone Swift

Auteur : IT Sectr Publié le : 2026-03-19 Temps de lecture : 8 min

@MainActor est un acteur global dans le langage Swift qui garantit l'exécution du code sur le thread principal. Selon Apple Developer, 2024, @MainActor automatise le basculement sur le thread principal lors du travail avec l'UI, libérant le développeur de l'appel manuel à DispatchQueue.main.async. L'annotation est apparue dans Swift 5.5 avec le système async/await.

Points clés

  • @MainActor — un acteur global Swift pour garantir l'exécution sur le thread principal.
  • Le système async/await — la base sur laquelle @MainActor est construit.
  • Annotation de classe place automatiquement toutes ses méthodes sur le thread principal.
  • Contrairement à DispatchQueue.main, @MainActor vérifie le thread au niveau du compilateur.
  • Mises à jour UI — le principal domaine d'application de @MainActor en développement iOS.

Qu'est-ce que @MainActor ?

@MainActor est un acteur global en Swift qui combine les propriétés des acteurs avec la garantie d'exécution sur le thread principal de l'application. Il fait partie du système de concurrence Swift introduit dans Swift 5.5 avec async/await et la concurrence structurée. L'annotation permet au développeur de ne pas se soucier du basculement manuel de threads et réduit le nombre d'erreurs d'UI.

Définition et place dans Swift Concurrency

Un acteur en Swift est un type par référence qui isole son état et garantit qu'un seul thread peut le modifier. @MainActor est un acteur global spécial dont l'exécuteur est le thread principal. Tout code marqué avec @MainActor s'exécute sur le thread principal — même s'il est appelé depuis une tâche en arrière-plan.

Avant @MainActor, les développeurs basculaient manuellement vers le thread principal via DispatchQueue.main.async. C'était une source fréquente d'erreurs : les développeurs oubliaient de basculer, provoquant des crashes dus à des mises à jour de l'UI en dehors du thread principal. @MainActor résout ce problème au niveau du système de types.

Raisons de la création

La source de la plupart des bugs dans les applications iOS est l'insécurité de l'UI — la mise à jour de l'interface depuis un thread secondaire. Apple a intégré @MainActor dans Swift Concurrency pour rendre le basculement vers le thread principal automatique et vérifiable par le compilateur, éliminant toute une classe d'erreurs d'exécution.

Comment fonctionne @MainActor ?

Le principe de fonctionnement de @MainActor est basé sur le système d'exécution de Swift Concurrency. Lorsqu'un thread appelle une fonction marquée avec @MainActor, l'ordonnanceur la suspend sur l'exécuteur actuel et la reprend sur le thread principal. Le compilateur suit les limites d'appel et garantit la sécurité.

Exécuteur du thread principal

L'exécution de @MainActor est gérée par MainActor.shared — un exécuteur associé au thread principal de l'application. Lorsqu'une fonction asynchrone est marquée avec @MainActor, elle reprend toujours sur cet exécuteur, quel que soit le thread sur lequel la tâche originale a été démarrée.

swift
import SwiftUI

class ViewModel: ObservableObject {
    @Published var items: [String] = []

    @MainActor
    func loadData() async {
        let result = await fetchRemoteData()
        items = result  // en toute sécurité, MainActor garantit le thread principal
    }
}

Héritage du contexte d'acteur

Si une fonction est marquée avec @MainActor et appelle une autre fonction asynchrone, elle hérite du contexte d'acteur par défaut. Cela signifie que tous les appels imbriqués s'exécutent également sur le thread principal, sauf indication contraire. Le compilateur suit cela et émet une erreur en cas de tentative de passage d'une fermeture incohérente.

@MainActor vs DispatchQueue.main

La comparaison de @MainActor et DispatchQueue.main aide à comprendre pourquoi le nouveau mécanisme est considéré comme plus sûr et plus pratique, bien que tous deux résolvent la même tâche — exécuter du code sur le thread principal.

Sécurité au niveau des types

@MainActor est une vérification au niveau du compilateur. Si vous essayez d'appeler une fonction @MainActor depuis un contexte non sécurisé, le compilateur émettra un avertissement ou une erreur. DispatchQueue.main.async est un appel d'exécution : le code sera compilé mais peut planter à l'exécution en cas de tentative de mise à jour de l'UI depuis un thread secondaire.

Performances et surcharge

DispatchQueue.main.async ajoute un bloc à la file d'attente qui peut être exécuté avec un retard. @MainActor avec async/await effectue un changement direct d'exécuteur sans créer de fermetures inutiles. Cela réduit la surcharge et rend le temps d'exécution plus prévisible.

swift
// Ancienne approche
DispatchQueue.main.async {
    self.updateUI()
}

// Nouvelle approche avec @MainActor
@MainActor
func updateUI() {
    // s'exécute sur le thread principal
    self.label.text = "Mis à jour"
}
Critère@MainActorDispatchQueue.main
Vérificationcompilateurexécution
Syntaxeannotation (déclarative)appel (impérative)
Surchargefaible (changement d'exécuteur)moyenne (fermeture + file)
Testabilitéélevée (MainActor.shared remplaçable)faible (difficile à simuler)

Utilisation de @MainActor dans les projets iOS

Dans les projets iOS réels, @MainActor est utilisé dans les couches ViewModel, les vues SwiftUI et les contrôleurs UIKit. L'annotation peut être appliquée à la fois à des méthodes individuelles et à un type entier.

Annotation de classe ou de structure

En marquant une classe avec @MainActor, vous garantissez que toutes ses méthodes et propriétés sont accessibles uniquement sur le thread principal. C'est particulièrement pratique pour les vues SwiftUI et les classes ObservableObject : vous ajoutez simplement @MainActor avant la classe, et toutes les propriétés @Published se mettent à jour en toute sécurité.

swift
@MainActor
final class UserListViewModel: ObservableObject {
    @Published var users: [User] = []
    @Published var isLoading = false

    func fetchUsers() async {
        isLoading = true
        users = await api.getUsers()
        isLoading = false
    }
}

Encapsulation du code legacy

Lorsque vous travaillez avec du code UIKit legacy où le basculement de threads était manuel, vous pouvez utiliser MainActor.run pour un basculement explicite. C'est pratique pour une transition incrémentale vers Swift Concurrency sans réécrire toute la base de code.

swift
await MainActor.run {
    self.tableView.reloadData()
}

Limitations de @MainActor

Malgré tous ses avantages, @MainActor présente un certain nombre de limitations qu'il est important de prendre en compte lors de la conception de l'architecture de l'application. Comprendre les limites d'applicabilité aide à éviter une utilisation incorrecte.

Performances en cas d'utilisation intensive

Si toute la chaîne d'appels est marquée avec @MainActor, alors tout travail lourd sera effectué sur le thread principal, provoquant des blocages de l'UI. Il est recommandé de marquer uniquement la couche UI avec @MainActor, en laissant la logique métier et les requêtes réseau dans des acteurs d'arrière-plan ou l'exécuteur global.

Incompatibilité avec certaines API

Les anciennes API basées sur des callbacks (par exemple, URLSession sans async/await) ne supportent pas le contexte d'acteur. L'intégration nécessite un wrapper avec CheckedContinuation. De plus, @MainActor n'est pas compatible avec performSelector, target-action et d'autres motifs non asynchrones d'UIKit.

Débogage multithread

Lors du débogage d'applications avec @MainActor, il est plus difficile de reproduire les conditions de concurrence car le compilateur en empêche beaucoup au moment de la compilation plutôt qu'à l'exécution. Cependant, cela peut créer un faux sentiment de sécurité : un travail incorrect avec des objets mutables partagés (par exemple, NSCache ou des variables globales partagées) reste possible s'ils ne sont pas marqués avec @MainActor et utilisés sans synchronisation explicite.

Tester @MainActor

@MainActor simplifie considérablement les tests de logique UI car il élimine la nécessité de basculer manuellement les threads dans les tests. Cependant, il existe des particularités à prendre en compte lors de l'écriture de tests unitaires et de tests UI.

Tests unitaires avec MainActor

Dans XCTest, l'environnement de test configure automatiquement l'exécuteur du thread principal. Lorsqu'une méthode de test s'exécute sur le thread principal, l'appel de fonctions @MainActor ne nécessite aucune configuration supplémentaire — elles s'exécutent dans le même contexte. Pour tester les scénarios d'arrière-plan, utilisez MainActor.run dans une Task avec une priorité et un exécuteur explicites, en vérifiant séparément que le code fonctionne correctement lorsqu'il est appelé depuis l'arrière-plan.

Une approche courante consiste à tester ViewModel avec @MainActor, où l'on vérifie que les propriétés @Published se mettent à jour correctement après des opérations asynchrones. Grâce à l'héritage du contexte d'acteur, l'appel de await à l'intérieur du test garantit l'exécution sur le thread principal sans garanties supplémentaires de DispatchQueue ni basculement manuel de contexte, ce qui simplifie l'écriture des tests.

Vérification de l'isolation lors du refactoring

Lors du refactoring de code existant vers Swift Concurrency, vérifiez l'isolation de @MainActor via le compilateur : tout appel à des méthodes synchrones sans @MainActor depuis un contexte @MainActor est marqué comme une erreur. Cette propriété est utilisée pour migrer progressivement un projet vers async/await : vous marquez la couche ViewModel comme @MainActor, et le compilateur met en évidence tous les appels non sécurisés qui doivent être déplacés vers des acteurs d'arrière-plan.

Mocking et contexte d'acteur

Lors de la création de mocks pour les dépendances @MainActor, utilisez des protocoles avec des méthodes async qui déclarent des fonctions asynchrones avec des types de retour. Cela permet de remplacer les services réseau, les bases de données et autres dépendances externes sans briser l'isolation de l'acteur. Le compilateur vérifie que le mock implémente toutes les exigences d'isolation, empêchant tout accès accidentel au code @MainActor depuis des threads de test en arrière-plan.

Attente pour les opérations asynchrones

Lors du test synchrone de code @MainActor, utilisez XCTestExpectation pour attendre la fin des opérations asynchrones. Définissez l'attente dans le test et appelez fulfillment à l'intérieur d'une fermeture qui s'exécute sur le thread principal. Si le test se bloque indéfiniment — l'appel sur le thread principal ne se produit probablement pas, et vous devez vérifier l'isolation de l'acteur. Pour déboguer le contexte d'exécution, il est utile d'ajouter une vérification Thread.isMainThread dans le code de test.

Questions fréquentes

Est-il nécessaire de marquer toute la classe avec @MainActor ?

Non, il suffit de marquer uniquement les méthodes qui mettent à jour l'UI. Cependant, si une classe a plusieurs telles méthodes, il est plus simple d'ajouter @MainActor à toute la classe. Cela garantit que tous ses membres s'exécutent sur le thread principal et simplifie la maintenance du code.

En quoi @MainActor diffère-t-il de @globalActor ?

@MainActor est une instance spécifique d'un acteur global liée au thread principal. @globalActor est un protocole pour créer vos propres acteurs globaux. Par exemple, vous pouvez créer un @BackgroundActor pour exécuter du code sur un thread secondaire si l'architecture du projet l'exige.

Peut-on utiliser @MainActor sans async/await ?

Oui, les fonctions synchrones avec @MainActor s'exécutent également sur le thread principal. Cependant, la valeur principale de @MainActor se révèle avec async/await, lorsqu'une fonction asynchrone reprend automatiquement sur le thread principal sans basculement manuel via DispatchQueue.main.

Comment annuler une tâche @MainActor ?

Task.cancel() fonctionne avec les tâches @MainActor de la même manière qu'avec les tâches normales. Une tâche @MainActor peut vérifier Task.isCancelled ou lancer CancellationError. Lors de l'annulation, le thread principal n'est pas bloqué — la tâche cesse simplement son exécution au point de suspension le plus proche.

Que se passe-t-il si @MainActor est appelé depuis un thread secondaire ?

Le compilateur garantit la sécurité : si vous appelez une fonction @MainActor depuis un contexte secondaire, le compilateur signalera l'erreur. Pour les appels asynchrones, il suffit de marquer le code appelant avec await, et l'exécuteur basculera lui-même sur le thread principal. Pour les appels synchrones, un basculement explicite via MainActor.run est requis.

Résumé

  • @MainActor — un acteur global Swift qui garantit l'exécution sur le thread principal.
  • Vérification du compilateur élimine toute une classe d'erreurs de sécurité UI.
  • Annotation de classe complète place automatiquement toutes ses méthodes sur le thread principal.
  • MainActor.run — basculement explicite pour le code legacy et les contextes synchrones.
  • Contrairement à DispatchQueue.main, @MainActor ne crée pas de fermetures et utilise le changement d'exécuteur.
  • Les calculs lourds ne doivent pas être effectués sous @MainActor pour éviter les blocages UI.
  • L'héritage du contexte d'acteur simplifie les chaînes d'appels asynchrones et rend le code cohérent, prévisible et sûr pour l'UI.

Nous développerons une application mobile clé en main

IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.

Discuter du projet

Lisez aussi