Most Swift 6 migrations stall in the same place. You turn on strict concurrency, get 400 errors, start sprinkling @MainActor on things, rebuild, and get 450 errors. The count goes up because every annotation you add propagates constraints into callers you hadn’t looked at yet.

Swift 6.2’s default actor isolation setting inverts that. Instead of assuming your code is nonisolated and making you prove otherwise, the compiler assumes everything is on @MainActor unless you say it isn’t. For a UI app, that assumption is already true. You’re just telling the compiler what was already the case.

It’s one build setting, and on UI-heavy codebases it removes most of the annotation noise before you start the real work.

What the Setting Actually Does

Without it, an unannotated declaration is nonisolated: callable from any thread, and the compiler enforces that anything it touches is safe from any thread. With it, an unannotated declaration is @MainActor, and you opt out explicitly with nonisolated or by assigning a different actor.

// With default isolation set to MainActor:

class ProfileViewModel {          // implicitly @MainActor
    var name = ""                 // implicitly @MainActor
    func refresh() { }            // implicitly @MainActor

    nonisolated func cacheKey() -> String {   // opted out, runs anywhere
        "profile"
    }
}

New projects created in Xcode 26 and later already have this on. Existing projects don’t. That’s the important part: if you’ve been fighting a migration on an older project and wondering why your experience doesn’t match the tutorials, this is often why. The tutorials were written against the new default.

Turning It On

In Xcode, it’s a Swift Compiler build setting: “Default Actor Isolation”, set to MainActor.

For Swift packages, the project setting doesn’t reach them. Each target needs it explicitly:

// swift-tools-version: 6.2

.target(
    name: "ProfileFeature",
    swiftSettings: [
        .defaultIsolation(MainActor.self)
    ]
)

The tools version matters. Below 6.2 the defaultIsolation setting isn’t available and you’ll get a manifest error that doesn’t obviously point at the version line.

Why This Fixes the Error Avalanche

The old default fights the shape of a typical app. View controllers, view models, presenters, coordinators, most of your formatting helpers: all of it runs on the main thread, always did, and was never intended to be called from anywhere else. Under nonisolated-by-default the compiler has no way to know that, so it treats every one of those types as potentially concurrent and asks you to prove each is safe.

Flipping the default means the compiler starts from the same assumption you do. The errors that remain are the ones that actually matter: the places where main-thread code touches something that genuinely runs elsewhere. That’s a much smaller and much more interesting list.

Concretely, on a UI-heavy target I’d expect the error count to drop by most of its volume, and the remainder to concentrate around three things: network layers, disk caches, and anything holding a Timer or a delegate callback from an old C-based API.

When You Shouldn’t Turn It On

Per-target is the key phrase. The setting is per target, and the right answer differs per target.

Turn it on for anything UI-facing. Leave it off for:

  • Server-side Swift. There’s no main thread to speak of, and defaulting to a single actor is exactly wrong for a request-per-task workload.
  • Networking and persistence layers. Code that’s genuinely concurrent by design shouldn’t pretend to be main-actor. You’d immediately annotate half of it nonisolated, which is the setting fighting you.
  • Shared model packages with no UI. Same reasoning.

A layered app usually ends up with MainActor default in the feature and UI modules, and the nonisolated default in the core and networking modules. That split reads well in the package manifest and it documents intent better than a scattering of annotations does.

Swift 6.3 Sands Off More Edges

Swift 6.3 arrived with the WWDC26 cycle, and it doesn’t change the model. It removes specific things that stall migrations:

  • weak let. A class that needed @unchecked Sendable purely because it had a weak var can now use weak let and pass Sendable checking honestly. Every @unchecked you delete is a promise you no longer have to keep by hand.
  • Explicit ~Sendable. You can declare that a type is deliberately not Sendable, without blocking subclasses from being Sendable themselves. In inherited UIKit hierarchies this puts the intent in the code instead of a comment.
  • A warning for swallowed Task errors. Task { try await something() } with nobody collecting the error now warns. This one is worth the upgrade on its own; that pattern is everywhere and it fails silently.
  • async in defer. Async cleanup without contortions.

There’s also @diagnose, which lets you change warning behavior on a specific declaration rather than a whole module:

@diagnose(.ignore, .deprecatedDeclaration)
func legacyBridge() {
    // deliberate debt, silenced here and only here
}

That’s more useful than it sounds. The failure mode of a big migration is a wall of warnings the team learns to scroll past, taking the important ones with it. Confining the noise to the exact declarations where the debt is intentional keeps the rest of the warnings meaningful.

The Order I’d Do It In

  1. Set the tools version to 6.2 and enable default MainActor isolation on UI targets only.
  2. Rebuild and read the remaining errors. They’re now signal.
  3. Push genuinely concurrent code behind nonisolated or its own actor, rather than annotating outward from the error site.
  4. Only then turn on full strict concurrency checking.

Step 3 is where the judgment is. The temptation when you see an error is to add an annotation at the point the compiler complained, which moves the problem one level up and gives you a new error tomorrow. The question to ask instead is which actor this code genuinely belongs to, then say that once.

If you’re migrating an older UIKit app, the other thing on your plate this cycle is the scene life cycle requirement, which blocks launch entirely on iOS 27. That one’s mechanical, so I’d clear it first: see iOS 27 requires UIScene.

Frequently Asked Questions

Does default MainActor isolation make my app slower?

Not in itself. It changes where the compiler thinks code runs, not how much work happens. If enabling it moves genuinely background work onto the main thread, that’s a design problem the setting surfaced rather than caused, and the fix is nonisolated or a dedicated actor on that code.

Is this the same as annotating everything @MainActor?

Effectively yes for unannotated declarations, but without the annotations. The difference is maintenance: the intent lives in one build setting per target instead of hundreds of attributes that drift as people add files.

Do new Xcode projects have this enabled already?

Yes. Projects created in Xcode 26 and later default to MainActor isolation. Existing projects keep the old nonisolated default until you change the setting, which is why migration advice written for new projects often doesn’t match what an older codebase does.

Does the Xcode setting apply to my Swift packages too?

No. It applies only to the project’s own targets. Each package target needs .defaultIsolation(MainActor.self) in its Swift settings, with the package tools version at 6.2 or later.

Should I enable it before or after turning on strict concurrency checking?

Before. Enabling it first is what keeps the strict-checking error list small enough to read. Doing it the other way round means triaging hundreds of errors that the setting would have made disappear anyway.