Actors, Isolation and the MainActor

35.1 Actors

Actors are reference types that protect their mutable state with isolation. External access is async (with an implicit mailbox/serial executor).

actor BankAccount {
    let id: UUID = UUID()
    private(set) var balance: Decimal = 0

    func deposit(_ amount: Decimal) {
        balance += amount                        // no data races: one task at a time inside the actor
    }

    func transfer(_ amount: Decimal, to other: BankAccount) async throws {
        guard balance >= amount else { throw BankError.insufficientFunds }
        balance -= amount
        await other.deposit(amount)              // crossing into another actor requires await
    }

    nonisolated var description: String { "Account \(id)" }   // accessible without await (immutable state only)
}

let account = BankAccount()
await account.deposit(100)
print(await account.balance)

35.2 Actor Reentrancy

After every await inside an actor, re-check your assumptions. Other calls may have run in between (actors are reentrant).

actor ImageCache {
    private var cache: [URL: Image] = [:]
    private var inFlight: [URL: Task<Image, Error>] = [:]

    func image(for url: URL) async throws -> Image {
        if let cached = cache[url] { return cached }
        if let task = inFlight[url] { return try await task.value }   // de-duplicate concurrent requests
        let task = Task { try await download(url) }
        inFlight[url] = task
        let image = try await task.value        // ⚠️ suspension: state may change while waiting (reentrancy)
        cache[url] = image
        inFlight[url] = nil
        return image
    }
}

35.3 The MainActor and Global Actors

@MainActor                                       // all members run on the main thread (UI)
final class ProfileViewModel: ObservableObject {
    @Published var name = ""

    func load() async {
        let user = try? await fetchUser(id: 1)   // runs off-main while awaiting network work
        name = user?.name ?? "Unknown"           // back on the main actor: safe UI update
    }
}

func updateUI() async {
    await MainActor.run { label.text = "Done" }  // hop to the main actor
}

@globalActor
actor DatabaseActor {
    static let shared = DatabaseActor()
}
@DatabaseActor func save(_ record: Record) { /* isolated to DatabaseActor */ }

35.4 Isolation Control

Opt out of actor isolation (only immutable or Sendable state) isolated parameter

func f(account: isolated BankAccount) runs on that actor #isolation / isolation: isolated (any Actor)? = #isolation Inherit the caller's isolation

Import/conform to pre-concurrency APIs with relaxed checking

MainActor.assumeIsolated { } when you know you're on the main thread Swift 6.2 approachable concurrency

Optional default @MainActor isolation for app targets, nonisolated(nonsending) async functions run on the caller's actor by default, and @concurrent explicitly offloads work to the concurrent pool

// Swift 6.2
@concurrent
func decodeLargeFile(_ url: URL) async throws -> Model {   // explicitly runs off the caller's actor
    try JSONDecoder().decode(Model.self, from: Data(contentsOf: url))
}