iOS / Swift

Swift 6 strict concurrency default isolation footguns: how teams accidentally @MainActor their whole app (and a CI check to prove it)

Swift 6’s strict concurrency checks get teams over the line, but one build setting can quietly turn large parts of your app into MainActor-only code. Here’s how that happens, how it shows up as UI jank, and a simple CI audit that catches “main-actor creep” early.

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 @MainActor to satisfy isolation and Sendable constraints.

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 @MainActor in “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 MainActor was 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:

  1. 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)
  1. The compiler surfaces a mix of Sendable issues, global mutable state issues, and actor-isolation issues.
  1. Someone discovers that setting Default Actor Isolation to MainActor makes a big portion of the errors disappear (especially in UI targets).
  1. 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 await or to become @MainActor too.
  1. 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 nonisolated function.
  • 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 -showBuildSettings for your scheme/configuration.
  • Parse out SWIFT_DEFAULT_ACTOR_ISOLATION per target.
  • Maintain an allowlist of UI targets that are permitted to be MainActor by default (if you really want that).

The policy can be as strict as you like:

  • Disallow MainActor default 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:

  1. Default Actor Isolation = nonisolated for most targets.
  2. Use @MainActor deliberately at the UI boundary (views, view models where appropriate, UI coordinators).
  3. 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.

More From Our Blog

Android 16 (API 36) edge-to-edge is enforced: the window insets contract your UI must satisfy
Mobile Engineering • Oct 16, 2026

Android 16 (API 36) edge-to-edge is enforced: the window insets contract your UI must satisfy

Targeting Android 16 (API 36) removes the practical opt-out from enforced edge-to-edge. The only stable fix is to define a single owner for each inset (status bars, navigation bars, display cutout, IME) and test for double-application in CI.

Read more →
TestFlight ‘Waiting for Review’ for days? Ship anyway with a dual‑lane release pipeline (and what to automate in App Store Connect)
Mobile Engineering • Oct 15, 2026

TestFlight ‘Waiting for Review’ for days? Ship anyway with a dual‑lane release pipeline (and what to automate in App Store Connect)

When TestFlight Beta App Review and App Store Review queues stall, you don’t need to miss your launch date. This playbook describes a dual‑lane pipeline (internal TestFlight vs external TestFlight vs App Store), deterministic rollout rules, and the App Store Connect automation to keep shipping under review latency.

Read more →
Swift 6.2 Approachable Concurrency in production: which Xcode 26 flags actually cut warning noise (without hiding races)
Mobile Engineering • Oct 13, 2026

Swift 6.2 Approachable Concurrency in production: which Xcode 26 flags actually cut warning noise (without hiding races)

Swift 6.2’s “Approachable Concurrency” switches on a small set of upcoming features that can dramatically change the shape of strict-concurrency diagnostics. This guide maps the Xcode 26 toggles to the warning classes they affect, and gives a staged rollout that keeps CI meaningful.

Read more →