Swift 6 strict concurrency is supposed to make data races harder. In practice, one project-level setting can quietly make your codebase behave as if it’s annotated @MainActor everywhere, and you only notice when scrolling starts stuttering.
This post explains the failure mode, how to reproduce and audit it, and one CI rule that prevents “main-actor creep” from becoming a late-stage performance regression.
The setting that changes the meaning of unannotated code
Swift 6.2 introduced a way to control default actor isolation: when a declaration has no explicit isolation annotation, the compiler can treat it as nonisolated or as isolated to a global actor such as MainActor. This is described in the Swift Evolution proposal that was implemented in Swift 6.2. (github.com)
In Xcode, this is exposed as a build setting named SWIFT_DEFAULT_ACTOR_ISOLATION in Apple’s build settings reference. (developer.apple.com)
In new-project defaults (and in many migrations), teams end up with:
- Swift Language Version set to Swift 6
- Strict concurrency checking effectively “complete” (Swift 6 mode turns many warnings into errors)
- Default Actor Isolation set to
MainActor
That last line is the footgun.
When default isolation is MainActor, any unannotated type/method can become main-actor isolated by default. From there, a couple of common patterns cause isolation to spread:
- A “root” type is
@MainActor(often for UI safety). - Other code calls into it, conforms to protocols it defines, or stores it.
- The compiler pushes you towards making more code
@MainActorto satisfy isolation andSendableconstraints.
On Swift Forums you’ll find people diagnosing surprising compiler errors by pointing at this setting, and noting that it’s the default for new projects in Xcode 26. (forums.swift.org)
The real-world symptom: you didn’t just silence warnings, you serialised work
MainActor isolation is a correctness tool, not a performance tool. When you unintentionally end up with large swathes of non-UI code being MainActor-isolated, the failure mode is usually one of these:
- UI responsiveness regressions: work that used to run off the main thread now executes on the main actor, competing with input, rendering, layout.
- Illusory concurrency: code uses
async/await, but the tasks frequently hop back to the main actor because the callee is main-actor isolated. - Contagious annotations: you start adding
@MainActorin “random” places to make errors go away, and the codebase converges on a single-threaded model with extra syntax.
This isn’t hypothetical; practitioners are actively discussing how default isolation set to MainActor changes the ergonomics (and behaviour) of a project, including the idea that many teams set it back to nonisolated early in a new app. (reddit.com)
The important nuance: there are cases where a MainActor default is a reasonable local optimisation for migration ergonomics (get the build green, then peel work off). There are also cases where it’s actively the wrong end state.
When the answer is “do not do this”
Avoid SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor as a steady-state choice when:
- You have meaningful CPU work (media, crypto, ML, large JSON, diffing, indexing, search) in the app process.
- You rely on parallelism for throughput (sync engines, background refresh, batch imports).
- You have multiple modules where only one is UI-facing.
- You ship SDK-style code or shared libraries;
-default-isolation MainActorwas not intended for libraries, and the ecosystem implications are part of ongoing evolution discussions. (github.com)
How teams accidentally MainActor their whole app
The common story looks like this:
- You flip on Swift 6 language mode and strict concurrency because Xcode is increasingly pushing it, and because “warnings as errors” forces progress. Apple’s guidance on adopting Swift 6 strict concurrency exists, and teams are actively following it. (developer.apple.com)
- The compiler surfaces a mix of
Sendableissues, global mutable state issues, and actor-isolation issues.
- Someone discovers that setting Default Actor Isolation to
MainActormakes a big portion of the errors disappear (especially in UI targets).
- The build goes green, but now:
- Any unannotated helper methods, protocol requirements, or model types in that target are implicitly main-actor isolated.
- Any code that touches them is forced to
awaitor to become@MainActortoo.
- You ship. Weeks later, someone reports that a screen “feels heavier”, but Instruments shows less obvious background thread work than expected.
On forums, you can see variants of this confusion: developers hit isolation errors and are told to check the default actor isolation build setting, because it often explains why a conformance or a seemingly-neutral type is suddenly main-actor isolated. (forums.swift.org)
A reproducible audit: find where MainActor is coming from
Treat this like any other production regression: make it observable, then make it enforceable.
Step 1: confirm the build setting per target
In Xcode build settings (or via xcodebuild -showBuildSettings), look for:
SWIFT_DEFAULT_ACTOR_ISOLATION
Apple documents this setting in the build settings reference. (developer.apple.com)
If it’s MainActor in targets that contain non-UI code (networking, persistence, sync, parsing), that’s already suspicious.
Step 2: spot “implicit @MainActor” in compilation errors and warnings
You’ll see diagnostics with phrases like:
- “Main actor-isolated … cannot be used in nonisolated context”
- “this is an error in the Swift 6 language mode”
These are often your first clue that a type you didn’t annotate has become main-actor isolated due to defaults.
Step 3: create a tiny canary file to force the compiler to reveal isolation
Add a file (per target) that tries to use a few representative APIs from a nonisolated context. The goal is not to ship this code; it’s to force the compiler to tell you which symbols are unexpectedly @MainActor.
For example:
- Create a
nonisolatedfunction. - Call into your “pure” layers (network client, JSON decoder wrapper, repositories).
- Ensure the canary is compiled in CI but excluded from production builds if you prefer.
If these calls require await or produce main-actor isolation errors, you’ve found creep.
One CI check that catches main-actor creep early
You want a check that is:
- Cheap (runs in minutes)
- Deterministic (no runtime flakiness)
- Actionable (points at the target and the setting)
The simplest version is: fail the build if a non-UI target has `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor`.
Why this works: the build setting is the “root cause knob”. If it’s wrong, you can waste weeks chasing symptoms (random @MainActor annotations, protocol contortions, unnecessary awaits).
Example CI rule (shell)
- Run
xcodebuild -showBuildSettingsfor your scheme/configuration. - Parse out
SWIFT_DEFAULT_ACTOR_ISOLATIONper target. - Maintain an allowlist of UI targets that are permitted to be
MainActorby default (if you really want that).
The policy can be as strict as you like:
- Disallow
MainActordefault isolation everywhere. - Or allow it only in app/UI targets, never in framework targets.
This aligns with the Swift Evolution intent that -default-isolation MainActor is not a great default for libraries. (github.com)
Safer defaults and migration strategy
If your goal is shipping software that stays responsive under load, a pragmatic approach is:
- Default Actor Isolation =
nonisolatedfor most targets. - Use
@MainActordeliberately at the UI boundary (views, view models where appropriate, UI coordinators). - Move shared mutable state behind actors or explicit isolation where it belongs.
If you are mid-migration and need a stepping stone, you can temporarily set MainActor default isolation in the UI target only, but treat it as technical debt with an expiry date.
A useful mental model: the main actor is a scarce resource. Spend it only where you need UI correctness.
Where Codeversols fits (briefly)
When we help teams adopt Swift 6 strict concurrency, the work is rarely “turn the switch on”. It’s defining isolation boundaries across modules, deciding what should be MainActor, and building guardrails (like the CI rule above) so the codebase doesn’t drift back into unsafe or sluggish defaults.
Close
Swift 6 strict concurrency is worth doing, but “default actor isolation = MainActor” is not a harmless compiler convenience. It changes what unannotated code means, and it can serialise surprising amounts of your app on the main actor.
Make it visible with an audit, then make it enforceable with a CI rule that checks SWIFT_DEFAULT_ACTOR_ISOLATION per target. The earlier you catch main-actor creep, the less likely you are to debug it as a late, expensive performance problem.



