Default Actor Isolation: A Great Intention with New Challenges

Default Actor Isolation: A Great Intention with New Challenges

While the primary goal of Swift’s strict concurrency checking is noble, it has introduced significant friction in single-threaded scenarios. Developers often find themselves cluttering code with redundant Sendable or @MainActor declarations just to appease the compiler. The "Default Actor Isolation" feature in Swift 6.2 is set to dramatically improve this experience by cutting down on unnecessary boilerplate. In this post, I’ll introduce how Default Actor Isolation works and highlight a few critical edge cases to keep in mind once you adopt it.

What are we facing?

Many developers found that upon enabling Swift’s strict concurrency checks, code that previously ran perfectly was suddenly flagged with a barrage of warnings or errors. This was particularly frustrating for single-threaded code that—despite clearly running on the MainActor—failed to compile. Consequently, developers were forced to make numerous adjustments just to satisfy the compiler’s requirements.

This friction exists because, prior to Swift 6.2, the compiler’s default inference strategy was this: if a function or type did not have an explicit or inferred isolation domain, it was treated as non-isolated. This implies it could be accessed concurrently. Even if you knew a module would run almost exclusively on the MainActor, there was no way to communicate this collective fact to the compiler.

"Default Actor Isolation" (SE-0466) solves exactly this. It gives developers the ability to declare, at the target level, that all code within should be treated as running on the MainActor. Once configured, the compiler will implicitly infer any unannotated code as isolated to the @MainActor, significantly easing the development overhead.

How to solve this issue?

In Xcode 26, newly created projects have this option enabled by default, with the default isolation domain set to MainActor. If you wish to revert the project's Default Actor Isolation to its previous behavior, you can adjust this setting in the Build Settings.

.target(
    name: "CareLogUI",
    swiftSettings: [
        .defaultIsolation(MainActor.self), // set Default Actor Isolation 
    ]),

Stepping Out of the Isolation Domain

Some developers might notice that if you add the following code to a SwiftData template project in Xcode 26, it actually triggers a compiler error:

@Model
final class Item {
    var timestamp: Date
    
    init(timestamp: Date) {
        self.timestamp = timestamp
    }
}

// SwiftData ModelActor macro
@ModelActor
actor DataHandler {
    func createItem(timeStamp: Date) throws {
        let item = Item(timestamp: timeStamp)
        modelContext.insert(item)
        try modelContext.save()
    }
}

This issue arises because the template code in Xcode 26 sets "Default Actor Isolation" to MainActor. In the code snippet above, since DataHandler is defined as an actor, the Swift compiler respects its own specific isolation domain (rather than applying the default MainActor). However, the Item declaration lacks any specific annotations, leading the compiler to implicitly infer that it belongs exclusively to the MainActor (effectively adding a hidden @MainActor for us). Consequently, attempting to create an Item within a non-MainActor isolation domain like DataHandler violates strict concurrency safety principles.

The fix is straightforward: simply add the nonisolated keyword before the Item declaration. This instructs the Swift compiler to skip the default isolation inference for Item, allowing the type to function across different isolation domains.

@Model
nonisolated final class Item { // add nonisolated
    var timestamp: Date
    
    init(timestamp: Date) {
        self.timestamp = timestamp
    }
}

Of course, if you simply want to opt a specific property or method out of the default isolation domain, you can just prefix it with the nonisolated keyword.

class RunInMainActor {    
    var name: String = "example" // running on MainActor
    
    nonisolated func processData() async {
        // Opting out of the MainActor isolation domain (at the compiler level)
    }
    
    nonisolated var computedValue: String {
        // Non-isolated computed property (at the compiler level)
        return "computed"
    }
}

It’s important to note that Swift 6.2 has introduced a significant semantic change to nonisolated (specifically by making nonisolated(nonsending) the default behavior via the NonisolatedNonsendingByDefault feature). As a result, nonisolated asynchronous methods now inherit the caller's isolation domain instead of forcing a hop to a background executor as they did previously. If you truly need to force a method to execute on a background thread, you should use the @concurrent attribute instead:

class RunInMainActor {        
    @concurrent
    func guaranteedBackground() async {
        // running in background
    }
}

If you find yourself manually adding nonisolated to a vast majority of your code in a MainActor-default project, it might be time to reconsider whether this mode is right for your needs. One solution is to revert the project’s default isolation back to nonisolated. Another approach is to decouple that specific logic into a separate Target, maintaining the legacy non-isolated inference behavior for that module.

In a sense, while "Default Actor Isolation" eases the burden on developers, it also acts as a catalyst, encouraging more teams to embrace modular programming.