Overview
Swift 6.4 ships with Xcode 27 (September 2026). Swift 6.2 introduced approachable concurrency, and new app projects created in Xcode 26 or later turn on SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor and SWIFT_APPROACHABLE_CONCURRENCY = YES; existing projects keep nonisolated. Structured concurrency ties task lifetime to scope: a parent waits for its children, and cancellation flows down. Language rules are in swift.
Choose default isolation per target
Set the default isolation to MainActor in app and UI targets so view models and glue code need no annotations, and leave library, networking, and model targets nonisolated. In a package (tools-version 6.2) use .defaultIsolation(MainActor.self); on the compiler use -default-isolation MainActor (SE-0466).
@concurrentmarks an async function that must run off the caller’s actor, such as parsing a large payload.- With the
NonisolatedNonsendingByDefaultfeature (approachable concurrency turns it on), anonisolatedasync function runs on the caller’s actor instead of hopping to the global pool (SE-0461). Mark it@concurrentto get the old behavior. - In a target that is not
MainActorby default (existing projects keepnonisolated), mark view models and any type that touches UI state@MainActor; the annotation covers every member. Do the same in code that may move between targets.
Prefer structured tasks over Task { }
Two independent awaits written one after another run serially. Use async let to start a fixed number of independent calls together and a task group for a dynamic number. When a child’s error propagates out of the group body, the group cancels the remaining children.
func fetchAll(_ urls: [URL]) async throws -> [Data] {
try await withThrowingTaskGroup(of: Data.self) { group in
for url in urls { group.addTask { try await URLSession.shared.data(from: url).0 } }
return try await group.reduce(into: []) { $0.append($1) }
}
}Reserve Task { } for the boundary where synchronous code must start async work (a button action). In SwiftUI attach work with .task { } or .task(id:), which cancels when the view disappears or the id changes. A bare Task inside business logic outlives its caller, does not inherit cancellation, and hides its errors; if you must start one, store the handle so you can cancel() it from outside.
Handle cancellation cooperatively
Cancellation does not stop code by itself. Call try Task.checkCancellation() inside long loops and before expensive steps. Swift 6.4 lets await run inside a defer block to completion (SE-0493) and adds withTaskCancellationShield for cleanup that must finish even when the task is cancelled (SE-0504).
Bridge callbacks once with a checked continuation
Wrap a completion-handler API with withCheckedThrowingContinuation and resume exactly once on every path. If the callback can fire more than once, bridge it with an AsyncStream instead. The checked variant traps on a double resume and logs a leak warning when a resume is missing, which is the behavior you want.
func legacyFetch(id: String) async throws -> Data {
try await withCheckedThrowingContinuation { cont in
OldClient.shared.fetch(id: id) { data, error in
if let error { cont.resume(throwing: error) }
else if let data { cont.resume(returning: data) }
else { cont.resume(throwing: FetchError.empty) }
}
}
}Use an actor for shared mutable state that is not UI
An actor serializes access to its stored properties; callers await across the boundary. Actors are reentrant: other callers can run at every await inside an actor method, so re-check state after a suspension instead of assuming it is unchanged.
actor TokenBucket {
private var tokens = 100
func take() -> Bool {
guard tokens > 0 else { return false }
tokens -= 1
return true
}
}Mark members that read only immutable let state nonisolated so callers skip the hop. Replace serial DispatchQueues with actors; the compiler can verify actor isolation but not queue discipline. Put await at subsystem boundaries, not deep inside helpers.
Make boundary-crossing types Sendable honestly
A non-public struct or enum whose stored properties are all Sendable is Sendable automatically; a public one must declare it. A class needs immutability or a justified @unchecked Sendable with a comment naming its lock. nonisolated(unsafe) removes isolation checking for a global or stored property, so reserve it for bridging unannotated C or Objective-C code and say why it is safe. Model types from Core Data and SwiftData are not Sendable; pass NSManagedObjectID or PersistentIdentifier instead (core-data, swiftdata).