Overview

Swift 6 enforces actor isolation at compile time. Every mutable stored property must belong to exactly one isolation domain, and crossing that boundary requires await. This page covers how to define actors, annotate types with @MainActor, opt out with nonisolated, and satisfy the Sendable checker. Read swift first; the rules here extend the concurrency section.

Define an actor when mutable state needs shared access

Use actor instead of class whenever multiple concurrent callers read or write the same mutable state. The compiler serializes access automatically.

actor TokenBucket {
    private var tokens: Int
    private let max: Int
 
    init(max: Int) {
        self.max = max
        self.tokens = max
    }
 
    func consume() -> Bool {
        guard tokens > 0 else { return false }
        tokens -= 1
        return true
    }
 
    func refill() {
        tokens = max
    }
}
 
// Caller must await because access crosses actor boundary
let allowed = await bucket.consume()

An actor’s stored properties are only accessible from within its isolation domain without await. The compiler rejects synchronous cross-actor access.

Use @MainActor for all UI-bound state

Mark every type that reads or writes UIKit or SwiftUI state with @MainActor. This ensures updates run on the main thread and the compiler enforces it.

@MainActor
final class ProfileViewModel: ObservableObject {
    @Published var displayName: String = ""
    @Published var isLoading = false
 
    func refresh() async {
        isLoading = true
        defer { isLoading = false }
        displayName = try? await fetchDisplayName() ?? displayName
    }
}

@MainActor on a type propagates to all stored properties and methods. You do not need to annotate each member individually. For swiftui views, @MainActor is already inferred; you only need to annotate the view model.

Mark pure methods nonisolated to avoid unnecessary hops

A method that reads only immutable state does not need actor isolation. Mark it nonisolated so callers can invoke it synchronously without crossing the isolation boundary.

actor Config {
    private var overrides: [String: String] = [:]
    let environment: String  // immutable; safe to expose nonisolated
 
    init(environment: String) {
        self.environment = environment
    }
 
    nonisolated func describe() -> String {
        "Config(\(environment))"
    }
}

Do not mark mutating methods nonisolated. The compiler rejects any attempt to mutate actor-isolated state from a nonisolated context.

Conform to Sendable explicitly for types that cross boundaries

A value or reference that moves between actors must conform to Sendable. Structs with all-Sendable stored properties conform automatically. Classes need either immutability or a manual conformance with a justification comment.

// Automatic: all stored properties are Sendable
struct WorkItem: Sendable {
    let id: UUID
    let payload: Data
}
 
// Manual: requires a lock inside; justify why it is safe
final class Cache<Key: Sendable, Value: Sendable>: @unchecked Sendable {
    private let lock = NSLock()
    private var store: [Key: Value] = [:]
 
    func set(_ value: Value, for key: Key) {
        lock.withLock { store[key] = value }
    }
}

@unchecked Sendable silences the compiler without removing the obligation. Add a comment that explains the locking strategy. See swift-value-types for why structs are preferred and usually cheaper.

Avoid nonisolated(unsafe) in Swift 6 code

nonisolated(unsafe) opts a stored property out of isolation checking. It is the successor to @_unsafeInheritExecutor and equally dangerous: it moves the race-free obligation to the programmer. Reserve it for bridging with C or Objective-C APIs that have no Swift 6 annotations, and comment exactly why it is safe.

Cross actor boundaries at the outermost call site

Push await to the boundary between subsystems, not deep inside implementation code. An actor method that calls another actor creates a suspension chain; keep that chain as shallow as possible.

// Prefer: one await at the integration layer
func handlePurchase(item: PurchaseItem) async throws {
    let receipt = await store.record(item)    // actor call
    await analytics.track(.purchase(receipt)) // actor call
    await ui.confirmPurchase(receipt)         // MainActor call
}

Flat chains are easier to reason about and easier to test. A method that awaits several actors in sequence is a natural integration point for a test double.

Prefer actors over serial dispatch queues

DispatchQueue(label:) with a serial queue is the Objective-C solution to the same problem actors solve. New Swift 6 code should use actors. The compiler verifies actor isolation; it cannot verify queue discipline. Existing queue-based code should be wrapped in an actor during migration so the isolation is explicit.