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))
}