Expert Swift 6 code relies on precise isolation rules to avoid both data races and needless actor hops.
Every piece of Swift code has a static isolation: an actor instance, a global actor such as @MainActor, or nonisolated. The compiler uses it to decide where code runs and what it may touch.
Inheritance rules worth knowing:
Task { }inherits the isolation of where it is created (and captures the actor);Task.detached { }does not.- Closures passed to non-
@Sendableparameters inherit the caller's isolation. - Members of a
@MainActortype are main-actor isolated unless markednonisolated.
Where nonisolated async functions run changed in Swift 6.2. Historically (SE-0338) they always hopped to the global concurrent executor. With the NonisolatedNonsendingByDefault upcoming feature, they run on the caller's actor, which removes many Sendable errors; annotate a function `@concurrent` to explicitly run it off the actor. The spelling nonisolated(nonsending) states the caller-inheriting behavior explicitly.
Region-based isolation (SE-0414) tracks groups of non-Sendable values. A value in a "disconnected" region — freshly created, not reachable from an actor — can be transferred into another domain if the sender never uses it afterwards. The `sending` keyword (SE-0430) declares that a parameter or result is transferred this way: func enqueue(_ job: sending Job) accepts non-Sendable values the caller gives up.
isolated parameters (func f(on actor: isolated MyActor)) and #isolation let a function run on whatever actor it is passed, avoiding hops for generic utilities.
Example
final class Job { var log: [String] = [] } // non-Sendable
actor Worker {
private var jobs: [Job] = []
func enqueue(_ job: sending Job) { // caller gives the job up
job.log.append("queued")
jobs.append(job)
}
var count: Int { jobs.count }
}
func makeAndSend(to worker: Worker) async {
let job = Job() // disconnected region
job.log.append("created")
await worker.enqueue(job) // OK: transferred
// print(job.log) // error: 'job' used after being sent
}
func runOnCaller(_ a: isolated Worker) -> Int { // runs on the actor passed in
a.count // no await needed
}
let w = Worker()
await makeAndSend(to: w)
print(await runOnCaller(w)) // 1Remember
Task {}inherits isolation;Task.detached {}does notsendingtransfers a non-Sendable value the caller stops using- Swift 6.2 can run nonisolated async code on the caller's actor;
@concurrentopts out
Watch out for
- Reaching for
Task.detachedto "go off the main thread" when a@concurrentor nonisolated function expresses the intent safely.