Swift 6.2 et la concurrence en pratique : MainActor par défaut, en sortir à dessein
Swift 6.0 a fait des accès concurrents aux données une erreur de compilation et, pendant un an, l’a fait payer à tout le monde. Le vérificateur de concurrence stricte transformait du code d’UI ordinaire en un mur de violations de Sendable et d’erreurs « main actor-isolated property can not be referenced from a nonisolated context ». Le diagnostic était correct (ce code pouvait réellement provoquer un accès concurrent), mais le volume enterrait le signal, et beaucoup d’équipes sont soit restées en mode Swift 5, soit ont saupoudré du @MainActor jusqu’à ce que les erreurs se taisent.
Swift 6.2 change la valeur par défaut plutôt que les règles. La garantie de sécurité contre les accès concurrents aux données est la même ; ce qui a changé, c’est le point de départ du compilateur. Adoptez les nouvelles valeurs par défaut et la plus grande partie du mur disparaît, parce que le compilateur suppose désormais ce qui était déjà vrai dans votre application : la majeure partie de votre code s’exécute sur le main actor, et vous le quittez à dessein, à des endroits nommés. C’est le modèle que je livre dans toutes les applications de la famille 9411, et c’est une pièce d’un arc d’évolution plus large que le tour d’horizon des nouveautés de Swift pour 2026 retrace à travers les propositions 6.3 et 6.4 qui ont suivi. Voici comment cela fonctionne, et les six erreurs qui mordent encore une fois que vous l’activez.
TL;DR
- SE-0466 vous permet d’isoler tout un module sur
@MainActorpar défaut. DéfinissezSWIFT_DEFAULT_ACTOR_ISOLATION = MainActoret cessez d’annoter chaque vue, modèle et view-model2. - SE-0461 fait que les fonctions
asyncnonisolated s’exécutent sur l’actor de l’appelant par défaut (nonisolated(nonsending)), de sorte qu’appeler du code asynchrone ne force plus un saut d’actor ni les erreurs deSendableà la traversée de frontière qui l’accompagnaient3. @concurrentest l’issue de secours. Marquez une fonction@concurrentpour pousser un travail CPU lourd (décodage, traitement d’image) sur un thread d’arrière-plan, délibérément et visiblement4.- Xcode 26 active les deux valeurs par défaut pour les nouveaux projets. L’interrupteur global est
SWIFT_APPROACHABLE_CONCURRENCY = YES5. - Le modèle inverse la charge : au lieu de prouver que chaque ligne peut sans danger s’exécuter hors du main, vous gardez tout sur le main et vous prouvez les rares endroits que vous quittez.
- Six erreurs concrètes survivent au changement. Les six ont des correctifs en une ligne une fois que vous voyez le schéma.
L’inversion, et pourquoi elle compte
Ancien modèle (Swift 6.0) : le code est nonisolated jusqu’à ce que vous l’isoliez. Chaque type qui touchait à un état d’UI avait besoin de @MainActor, chaque appel asynchrone traversant une frontière d’isolation avait besoin d’une conformité à Sendable, et le compilateur signalait chaque écart. Pour une application où 95 pour cent du code s’exécute déjà sur le thread principal, vous passiez votre temps à annoter ces 95 pour cent afin de décrire un fait qui n’a jamais été en doute.
Nouveau modèle (Swift 6.2) : le code est sur le main actor jusqu’à ce que vous le quittiez. SE-0466 vous permet de déclarer l’isolation sur le main actor comme valeur par défaut du module, de sorte qu’une vue, son modèle et ses helpers sont tous @MainActor sans une seule annotation2. SE-0461 supprime ensuite le second impôt : une fonction async nonisolated s’exécute désormais sur l’actor qui l’a appelée au lieu de sauter vers l’exécuteur global, si bien que l’attendre ne vous traîne plus à travers une frontière d’isolation ni n’exige Sendable sur tout ce qui se trouve dans la portée3.
Le modèle mental est celui qui correspond à la façon dont les applications d’UI se comportent réellement. Le thread principal par défaut n’est pas un compromis ; c’est la vérité d’une application dont l’état, ce sont ses vues. La concurrence devient l’exception vers laquelle vous tendez, nommée et contenue, plutôt que la condition ambiante contre laquelle vous vous défendez à chaque ligne. Le travail du compilateur bascule de « prouvez que ceci peut sans danger s’exécuter de façon concurrente » à « vous avez dit que ceci s’exécute de façon concurrente, alors prouvez que c’est sûr », et la seconde question est posée à bien moins d’endroits.
L’équipe centrale de Swift l’a dit explicitement. Au Swift Group Lab de la WWDC 2026, Holly a décrit comment, en concevant le modèle de concurrence aujourd’hui, les fonctions asynchrones nonisolated se seraient exécutées dans le contexte de l’appelant dès le premier jour ; elle a paraphrasé qu’elle s’était longtemps accrochée au modèle de pool concurrent global et qu’elle a été convaincue du contraire par des problèmes rencontrés dans des projets réels. Doug a ajouté que l’ancienne valeur par défaut poussait trop de types vers Sendable. (Les deux citations sont paraphrasées à partir d’un enregistrement du lab transcrit localement ; Apple n’a publié aucun sous-titre pour la session.)9
L’activation
Deux paramètres de build, tous deux présents dans les valeurs par défaut de Xcode 26 pour les nouveaux projets et tous deux à définir explicitement sur un projet existant5 :
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor
SWIFT_APPROACHABLE_CONCURRENCY = YES
Le premier, c’est SE-0466 : le module est isolé par défaut sur le main actor. Le second est l’interrupteur global qui active l’ensemble de fonctionnalités de la concurrence approchable (approachable concurrency), y compris le comportement « l’appelant exécute » de SE-0461. Dans un package Swift, vous définissez les mêmes valeurs par défaut via swiftSettings avec les drapeaux de fonctionnalités à venir correspondants plutôt que via le paramètre de build de Xcode6.
Activez les deux sur un projet existant et le nombre d’erreurs chute brutalement, parce que l’essentiel de ce que le vérificateur signalait était du code du thread principal qu’il ne pouvait auparavant pas supposer être sur le thread principal. Ce qui reste est une courte liste de véritables cas-frontières. Il vaut la peine de les connaître par leur nom, car chacun est un endroit où votre code quitte réellement le main actor, et le correctif consiste à le dire avec précision.
Les six erreurs qui survivent au changement
Voici les erreurs de concurrence stricte qui apparaissent encore sous SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor, tirées de la migration des applications de la famille 941 vers ce modèle1. Chacune est une frontière réelle, pas un faux positif, et c’est pourquoi le correctif est une annotation précise plutôt qu’une suppression.
1. Fonctions pures sur des types Sendable. Une méthode pure sur une enum Sendable (un constructeur d’URL, un formateur) hérite de l’isolation sur le main actor sous la valeur par défaut du module, puis provoque une erreur lorsqu’elle est appelée depuis un contexte nonisolated : « Call to main actor-isolated instance method in a synchronous nonisolated context. » La méthode ne touche à aucun état, donc l’isoler sur le main actor est faux. Marquez-la nonisolated7 :
nonisolated func searchURL(for query: String) -> URL? { ... }
2. Statiques singletons dans les paramètres par défaut. static let shared = Foo() est isolé sur le main actor sous la valeur par défaut, mais les valeurs de paramètres par défaut sont évaluées dans le contexte de l’appelant, qui est souvent nonisolated : « Main actor-isolated static property ‘shared’ can not be referenced from a nonisolated context. » Rendez le statique nonisolated. Si le type est Sendable (ou @unchecked Sendable parce que vous protégez son état vous-même), vous n’avez besoin d’aucun qualificateur unsafe :
final class KeychainProxySecretStore: @unchecked Sendable {
nonisolated static let shared = KeychainProxySecretStore()
}
3. Constantes primitives immuables. Le même problème de paramètre par défaut frappe une simple constante : un static let defaultInterval référencé depuis un argument par défaut nonisolated. Le correctif est identique et la constante est trivialement sûre à partager :
nonisolated static let defaultInterval: TimeInterval = 15 * 60
4. Un corps de Task qui lit un self capturé. Une closure externe capture [weak self] ; à l’intérieur, un Task { @MainActor in self?.foo() } lit cet optionnel capturé : « Reference to captured var ‘self’ in concurrently-executing code. » La Task lit une liaison var de la portée englobante de façon concurrente, ce qui constitue l’accès concurrent. Recapturez self à la frontière de la Task pour que la Task possède une liaison immuable :
NotificationCenter.default.addObserver(...) { [weak self] _ in
Task { @MainActor [weak self] in
self?.value = next
}
}
5. Callbacks KVO qui lisent un état du main actor. Un callback webView.observe(\.canGoBack) { wv, _ in ... } est @Sendable et donc nonisolated, mais WKWebView.canGoBack est isolé sur le main actor : « Main actor-isolated property ‘canGoBack’ can not be referenced from a Sendable closure. » Le KVO délivre de façon synchrone sur le thread qui a muté la valeur, et l’état de navigation de WKWebView ne mute que sur le thread principal, donc la lecture est saine. Affirmez-le avec MainActor.assumeIsolated, qui supprime entièrement le saut de Task et reste synchrone8 :
let pushNav: @Sendable (WKWebView?) -> Void = { [weak self] webView in
MainActor.assumeIsolated {
guard let self else { return }
// safe to read webView?.canGoBack synchronously
}
}
assumeIsolated est une promesse faite au compilateur, pas une question. Ne l’utilisez que là où l’invariant d’exécution tient véritablement (un callback documenté sur le thread principal), car une fausse promesse est un crash, pas un avertissement.
6. Travail lourd qui ne devrait pas être sur le main. Celui-là, le vérificateur ne le signalera pas, et c’est le plus important à attraper vous-même. Sous une valeur par défaut sur le main actor, une méthode synchrone bornée par le CPU (décodage JSON d’une charge utile volumineuse, redimensionnement d’une image) s’exécute sur le main actor et fait sauter votre UI. La valeur par défaut vous garde sur le main ; @concurrent est la façon dont vous le quittez à dessein4 :
@concurrent
func decodeLargePayload(_ data: Data) async throws -> Report {
try JSONDecoder().decode(Report.self, from: data)
}
@concurrent décharge la fonction vers l’exécuteur global et est mutuellement exclusif avec @MainActor, un global actor personnalisé et nonisolated(nonsending), par conception : une fonction soit s’exécute là où se trouve son appelant, soit s’en éloigne délibérément, jamais de façon ambiguë4. La discipline que demande le nouveau modèle réside tout entière dans ce schéma. Restez sur le main pour tout ce qui touche à l’UI, et ne recourez à @concurrent que pour le travail qui a mesurablement besoin d’un thread d’arrière-plan.
Quand les valeurs par défaut ne vous conviennent pas
Le main actor par défaut convient aux applications : le code SwiftUI et UIKit est très majoritairement sur le thread principal, et la valeur par défaut correspond à la réalité. Elle convient moins bien dans deux cas, et prétendre le contraire vous fait perdre votre temps.
- Une cible de bibliothèque ou de framework sans UI. Une couche réseau, un parser ou un moteur de données n’a aucune raison d’être isolé par défaut sur le main actor, et le faire force
@concurrentounonisolatedsur presque tout. LaissezSWIFT_DEFAULT_ACTOR_ISOLATIONnon défini pour ces cibles et isolez délibérément, à l’ancienne. - Un système concurrent fortement basé sur les actors. Si votre conception exécute véritablement beaucoup de choses en parallèle (un vrai pipeline, pas une application avec quelques tâches d’arrière-plan), la valeur par défaut sur le main actor vous combat. Vous voulez des actors explicites et du code nonisolated, et la valeur par défaut de SE-0466 est le mauvais point de départ.
Pour une application, en revanche, le choix est facile : activez les deux paramètres, laissez le nombre d’erreurs s’effondrer, et traitez la poignée qui reste comme une carte de l’endroit exact où votre code quitte le thread principal. Cette carte vaut la peine d’être possédée. L’ancien modèle vous donnait mille avertissements et aucune carte ; le nouveau vous donne six frontières honnêtes et une valeur par défaut qui correspond enfin à la façon dont l’application s’exécute.
Une dernière note sur le bruit par rapport au signal. SourceKit affichera des erreurs d’index inter-fichiers (« Cannot find type X in scope », « No such module ») dans l’éditeur pendant que Xcode reconstruit son index, surtout juste après avoir régénéré un projet. Ce sont des artefacts d’index, pas des erreurs de concurrence. Si xcodebuild indique BUILD SUCCEEDED, le modèle de concurrence est satisfait et l’éditeur ne fait que rattraper son retard1. Courir après des fantômes d’index est le moyen le plus rapide de gâcher un après-midi sur une migration qui fonctionnait déjà.
FAQ
Comment activer la concurrence avec le main actor par défaut dans Swift 6.2 ?
Définissez deux paramètres de build, tous deux activés par défaut pour les nouveaux projets Xcode 26 : SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor et SWIFT_APPROACHABLE_CONCURRENCY = YES5. Dans un package Swift, vous définissez les drapeaux de fonctionnalités à venir équivalents via swiftSettings plutôt que via le paramètre de build de Xcode6.
Que change réellement SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor ?
Il rend le code d’un module isolé par défaut sur le main actor (SE-0466), de sorte qu’une vue, son modèle et ses helpers sont tous @MainActor sans aucune annotation2. La concurrence devient l’exception nommée vers laquelle vous tendez plutôt que la condition ambiante contre laquelle vous annotez à chaque ligne.
Qu’est-ce que @concurrent et quand devrais-je l’utiliser ?
@concurrent décharge une fonction async vers l’exécuteur global, à dessein, loin de l’actor de son appelant4. Sous une valeur par défaut sur le main actor, vous l’utilisez pour le travail qui a mesurablement besoin d’un thread d’arrière-plan (décodage d’une charge utile volumineuse, redimensionnement d’une image) ; il est mutuellement exclusif avec @MainActor, donc une fonction soit s’exécute là où se trouve son appelant, soit s’en éloigne délibérément.
Pourquoi est-ce que j’obtiens « Main actor-isolated … can not be referenced from a nonisolated context » ?
Sous la valeur par défaut sur le main actor, les fonctions pures, les statiques singletons et les constantes héritent de l’isolation sur le main actor, puis provoquent une erreur lorsqu’elles sont appelées depuis un contexte nonisolated, tel que l’évaluation d’un paramètre par défaut. Le correctif est une annotation nonisolated précise sur le membre, pas une suppression ; la valeur est sûre à partager parce qu’elle ne touche à aucun état d’actor7.
Devrais-je utiliser le main actor par défaut pour une cible de bibliothèque ou de framework ?
Non. Une couche réseau, un parser ou un moteur de données n’a aucune raison d’être isolé par défaut sur le main actor, et le faire force @concurrent ou nonisolated sur presque tout. Laissez SWIFT_DEFAULT_ACTOR_ISOLATION non défini pour les cibles sans UI et isolez délibérément, à l’ancienne.
Attendre une fonction async nonisolated provoque-t-il encore un saut d’actor dans Swift 6.2 ?
Non. SE-0461 fait qu’une fonction async nonisolated s’exécute sur l’actor qui l’a appelée au lieu de sauter vers l’exécuteur global, de sorte que l’attendre ne vous traîne plus à travers une frontière d’isolation ni n’exige Sendable sur tout ce qui se trouve dans la portée3.
-
Code de production de l’auteur réparti sur les applications iOS de la famille 941 (Ki, Return, Get Bananas), toutes livrées avec
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActoretSWIFT_APPROACHABLE_CONCURRENCY = YES. Les six schémas d’erreur et leurs correctifs ci-dessous constituaient l’ensemble complet de nettoyage de concurrence stricte pour Ki 1.0.0. ↩↩↩ -
Swift Evolution, SE-0466 : Control default actor isolation inference. Permet à un module d’utiliser par défaut l’isolation
@MainActor, de sorte que les cibles d’UI et d’application s’exécutent sur le main actor à moins que le code n’opte pour la sortie via@concurrentou un actor explicite. ↩↩↩ -
Swift Evolution, SE-0461 : Run nonisolated async functions on the caller’s actor by default. Une fonction
asyncnonisolated utilise par défautnonisolated(nonsending), s’exécutant sur l’actor de l’appelant au lieu de sauter vers l’exécuteur global, ce qui supprime les exigences deSendableà la traversée de frontière qui accompagnaient le saut. ↩↩↩ -
Swift Evolution, SE-0461 introduit l’attribut
@concurrentpour faire opter une fonction pour une exécution sur l’exécuteur global (un thread d’arrière-plan).@concurrentetnonisolated(nonsending)sont les deux modes d’isolation opposés d’une fonction async nonisolated : une fonction soit s’exécute là où se trouve son appelant, soit s’en éloigne délibérément.@concurrentne peut pas être combiné avec@MainActorou un global actor personnalisé. ↩↩↩↩ -
SWIFT_APPROACHABLE_CONCURRENCYest le paramètre de build global de Xcode qui active les fonctionnalités à venir de la concurrence approchable (y compris le comportement de SE-0461), etSWIFT_DEFAULT_ACTOR_ISOLATIONsélectionne l’isolation par défaut du module. Les nouveaux projets Xcode 26 activent les deux avec la valeur par défaut sur le main actor. Documenté chez Donny Wals, « Exploring concurrency changes in Swift 6.2 », et Paul Hudson, « What’s new in Swift 6.2 », tous deux recoupés avec les propositions sous-jacentes SE-0461 et SE-0466. ↩↩↩ -
Swift, Swift Concurrency Migration Guide, « Enabling Complete Concurrency Checking » et configuration du mode de langage. Dans un package Swift, l’isolation par défaut et les fonctionnalités de la concurrence approchable se définissent via les drapeaux de fonctionnalités à venir de
swiftSettingsplutôt que via le paramètre de build de Xcode. ↩↩ -
Swift, Migration Guide : global actor isolation and
nonisolated. Les méthodes d’un type@MainActorhéritent de l’isolation sur le main actor ;nonisolatedfait opter une méthode pour la sortie, ce qui est correct pour les fonctions pures qui ne touchent à aucun état isolé. ↩↩ -
Apple Developer,
MainActor.assumeIsolated(_:). Affirme que l’exécution courante est déjà sur le main actor et exécute la closure de façon synchrone sans saut d’actor. L’assertion provoque un trap à l’exécution si l’invariant ne tient pas, donc elle n’est valide que là où l’appelant est garanti d’être sur le thread principal. ↩ -
Apple, WWDC 2026 Swift Group Lab. Les propos attribués à Holly et Doug de l’équipe centrale de Swift sont paraphrasés à partir d’un enregistrement transcrit localement ; Apple n’a publié aucune transcription ni aucun sous-titre officiel pour la session, donc la formulation est approximative et non une citation textuelle. ↩