@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 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.
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.
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.
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é.
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.
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
}
}
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.
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.
@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.
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.
// 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 | @MainActor | DispatchQueue.main |
|---|---|---|
| Vérification | compilateur | exécution |
| Syntaxe | annotation (déclarative) | appel (impérative) |
| Surcharge | faible (changement d'exécuteur) | moyenne (fermeture + file) |
| Testabilité | élevée (MainActor.shared remplaçable) | faible (difficile à simuler) |
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.
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é.
@MainActor
final class UserListViewModel: ObservableObject {
@Published var users: [User] = []
@Published var isLoading = false
func fetchUsers() async {
isLoading = true
users = await api.getUsers()
isLoading = false
}
}
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.
await MainActor.run {
self.tableView.reloadData()
}
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.
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.
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.
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.
@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.
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.
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.
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.
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
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.
@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.
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.
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.
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é
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.
Lisez aussi