While Swift’s strict concurrency checking has good intentions, it significantly increases the burden on developers in many single-threaded scenarios. Developers are forced to add unnecessary Sendable
, @MainActor
, and other declarations to their code just to satisfy the compiler’s requirements. Swift 6.2’s new Default Actor Isolation feature will greatly improve this situation and reduce unnecessary boilerplate code. This article will introduce the Default Actor Isolation feature and point out some situations to be aware of when using it.
The Motivation
Many developers find that after enabling Swift’s strict concurrency checking, some previously well-functioning code will generate numerous warnings or even errors. This is especially true for single-threaded code that clearly runs on the MainActor
, yet still fails to compile. Developers need to make considerable adjustments to this code to satisfy the compiler’s requirements.
This situation occurs because, prior to Swift 6.2, the compiler’s default inference strategy for concurrency was: if a function or type is not explicitly declared or inferred to have an isolation domain, it is treated as non-isolated, meaning it can be used concurrently. Even when you know that most of the code in a module only runs on MainActor, there was still no unified way to communicate this fact to the compiler.
The Default Actor Isolation feature (SE-0466) was created to solve this problem. This feature provides developers with the ability to uniformly indicate to the compiler that code within a Target scope runs on MainActor
. Once configured, when compiling that Target’s code, the compiler will implicitly infer code without explicit isolation domain annotations to be isolated to @MainActor
, thereby reducing the developer’s burden.
Usage
In Xcode 26, newly created projects have this option enabled by default and set the default isolation domain to MainActor
. If you want to restore your project’s Default Actor Isolation to the previous behavior, you can modify it in Build Settings.
For SPM, the default non-isolated setting is still used. You can set a Target’s Default Actor Isolation to MainActor
in the following way:
.target(
name: "CareLogUI",
swiftSettings: [
.defaultIsolation(MainActor.self), // set Default Actor Isolation
]),
Escaping from the Isolation Domain
Some developers might notice that if you add the following code to an Xcode 26 SwiftData template project, it will generate an error:
@Model
final class Item {
var timestamp: Date
init(timestamp: Date) {
self.timestamp = timestamp
}
}
// New code using SwiftData ModelActor macro
@ModelActor
actor DataHandler {
func createItem(timeStamp: Date) throws {
let item = Item(timestamp: timeStamp)
modelContext.insert(item)
try modelContext.save()
}
}
This problem occurs because Xcode 26’s template code sets Default Actor Isolation to MainActor
. In the above code, since DataHandler
is an Actor
, the Swift compiler respects its isolation domain (rather than using the default MainActor), but the Item
declaration has no annotations, so the compiler implicitly infers it can only run on MainActor
(effectively treating the type as if @MainActor
were applied). Consequently, creating an Item
in a non-MainActor
isolation domain (DataHandler
) violates the principles of safe concurrency.
The solution is simple. Add nonisolated
before the Item
declaration, so the Swift compiler won’t apply default isolation inference to Item
. The Item
type can then run in different isolation domains.
@Model
nonisolated final class Item { // Add nonisolated
var timestamp: Date
init(timestamp: Date) {
self.timestamp = timestamp
}
}
Of course, if you only want to remove a specific property or method from the default isolation domain, you can directly add nonisolated
in front of it.
class RunInMainActor {
var name: String = "example" // Runs on MainActor
nonisolated func processData() async {
// Escapes MainActor isolation domain (at compile-time)
}
nonisolated var computedValue: String {
// Non-isolated computed property (at compile-time)
return "computed"
}
}
Important note: Due to significant semantic changes to nonisolated
in Swift 6.2 (introducing nonisolated(nonsending)
as the default behavior when NonisolatedNonsendingByDefault
is enabled), nonisolated
async methods now inherit the caller’s isolation domain rather than forcibly switching to background execution as before. If you truly need to force a method to execute on a background thread, you should use the @concurrent
annotation:
class RunInMainActor {
@concurrent
func guaranteedBackground() async {
// Guaranteed to execute on background thread
}
}
If you find yourself needing to add nonisolated
extensively in a project with Default Actor Isolation set to MainActor
, you should consider whether to continue using this pattern. One solution is to change the project’s default isolation to nonisolated
, another is to move this portion of the code into a dedicated Target Target that uses the previous nonisolated
default isolation inference approach.
From one perspective, Default Actor Isolation, while reducing developer burden, also encourages more people to adopt modular programming.
Default Actor Isolation for MainActor vs @MainActor Are Not Exactly the Same
Although in most cases we can treat setting Default Actor Isolation to MainActor
as automatically adding @MainActor
to unmarked types, there are still some differences between the two in certain situations. For example, consider this code:
@MainActor
class UserMainActorClass {
private final class DefaultsObservation: @unchecked Sendable {
private var notificationObserver: NSObjectProtocol?
deinit {
if let observer = notificationObserver {
NotificationCenter.default.removeObserver(observer)
}
}
}
}
This code compiles correctly when Default Actor Isolation is nonisolated
, but generates a compilation error when switched to MainActor
:
At this point, the compiler considers the deinit
of the internal nested type to not be in the same isolation domain. We must add isolated
or @MainActor
before deinit
to make the compiler treat this deinit
as running on @MainActor
.
class UserMainActorClass {
private final class DefaultsObservation: @unchecked Sendable {
private var notificationObserver: NSObjectProtocol?
isolated // or @MainActor
deinit {
if let observer = notificationObserver {
NotificationCenter.default.removeObserver(observer)
}
}
}
}
I’m not sure whether this difference is intentionally designed or a current defect, but at least in scenarios involving nested type declarations, Default Actor Isolation set to MainActor
is not completely equivalent to explicit @MainActor
.
New Challenges for Macro Authors
A few days ago, my ObservableDefaults macro received an Issue. A user reported that in Xcode 26, when Default Actor Isolation is set to MainActor
, a compilation error occurs:
@Sendable // ERROR: Main actor-isolated synchronous instance method 'userDefaultsDidChange' cannot be marked as '@Sendable'
When I first saw this Issue, I was somewhat puzzled because the ObservableDefaults macro implementation specifically handles @MainActor
. When it detects that a user-declared type is annotated with @MainActor
, it responds accordingly in the generated code. The following code is used in the macro to determine whether a type has been annotated with @MainActor
:
let hasExplicitMainActor = classDecl.attributes.contains(where: { attribute in
if case let .attribute(attr) = attribute,
let identifierType = attr.attributeName.as(IdentifierTypeSyntax.self)
{
return identifierType.name.text == "MainActor"
}
return false
})
Obviously, when Default Actor Isolation is set to MainActor
, the corresponding branch is not taken.
I also discovered the same phenomenon when testing in Xcode 26. When Default Actor Isolation is set to MainActor
, although the compiler uses default isolation domain inference, the macro cannot detect this situation.
Since I haven’t found a way to obtain Default Actor Isolation status within macros (and may never have one), I ultimately had to add a macro parameter, allowing users to explicitly indicate when Default Actor Isolation is set to MainActor
, so the macro can make judgments based on this parameter state:
@ObservableDefaults(defaultIsolationIsMainActor: true)
class Settings {
var name: String = "Fatbobman"
var age: Int = 20
}
This presents a new kind of challenge for macro authors introduced by Default Actor Isolation.
Conclusion
Xcode 26’s default setting of Default Actor Isolation to MainActor
may cause some confusion for developers in the short term, but once familiar with this feature, developers will appreciate this new change in Swift 6.2, as it facilitates the further adoption of Swift’s strict concurrency.
If this article helped you, feel free to buy me a coffee ☕️ . For sponsorship inquiries, please check out the details here.