Actors
Swift actors are reference types that protect mutable state from data races by serialising access. Method calls cross the actor boundary with await; the compiler enforces isolation. @MainActor + global actors integrate with UI threads. Modern Swift concurrency without locks.
Actor, MainActor, isolation, nonisolated
EXAMPLE
import Foundation
// 1) Basic actor — protects internal state
actor Counter {
private var count = 0
func increment() { count += 1 }
func value() -> Int { count }
}
let counter = Counter()
Task {
await counter.increment() // cross-actor calls need await
let v = await counter.value()
print(v)
}
// Inside the actor, methods access state synchronously.
// Outside the actor, access requires await.
// 2) Actors prevent data races
actor Cache {
private var data = [String: String]()
func get(_ key: String) -> String? { data[key] }
func set(_ key: String, value: String) { data[key] = value }
func clear() { data.removeAll() }
}
let cache = Cache()
// Concurrent access is safe:
Task { await cache.set("a", value: "1") }
Task { await cache.set("b", value: "2") }
Task { print(await cache.get("a") ?? "-") }
// The runtime queues method calls; one at a time inside the actor.
// 3) Nonisolated members — accessible without await
actor Counter2 {
let label: String // immutable; safe to access
private var count = 0
init(label: String) { self.label = label }
nonisolated func describe() -> String { "Counter \\(label)" }
nonisolated var displayName: String { "\\(label) counter" }
}
let c = Counter2(label: "main")
print(c.label) // OK — let is sendable + safe
print(c.describe()) // OK — nonisolated
// 4) @MainActor — bind state to the main thread
@MainActor
class AppViewModel: ObservableObject {
@Published var users: [User] = []
func load() async {
let result = try? await fetchUsers()
users = result ?? []
}
private func fetchUsers() async throws -> [User] {
let (data, _) = try await URLSession.shared.data(from: URL(string: "https://api.example.com/users")!)
return try JSONDecoder().decode([User].self, from: data)
}
}
// All methods of a @MainActor type run on the main thread.
// Modifying @Published is safe — happens on main → SwiftUI updates work correctly.
// 5) @MainActor on individual methods
class MyClass {
@MainActor
func updateUI(_ text: String) {
// guaranteed main thread
}
}
// 6) Cross-actor calls — always async
actor Logger {
private var logs = [String]()
func record(_ msg: String) { logs.append(msg) }
func all() -> [String] { logs }
}
@MainActor
func handleTap() async {
let logger = Logger()
await logger.record("tapped") // await — different actor
let entries = await logger.all()
print(entries)
}
// 7) Global actors — shared isolation
@globalActor
actor StorageActor {
static let shared = StorageActor()
}
@StorageActor
class DiskCache {
private var data = [String: Data]()
func get(_ key: String) -> Data? { data[key] }
func set(_ key: String, _ value: Data) { data[key] = value }
}
// All DiskCache instances share the StorageActor's executor → serialised access.
// 8) Sendable — types crossing actor boundaries
struct UserData: Sendable {
let id: Int
let name: String
}
actor UserService {
func fetch(id: Int) -> UserData {
return UserData(id: id, name: "Mara")
}
}
// Returning Sendable types is safe across actors.
// Classes with mutable state are NOT Sendable — wrap them in actors or use @unchecked Sendable (careful).
// 9) Reentrancy — actor methods can suspend + others run
actor Bank {
private var balance: Int = 100
func withdraw(_ amount: Int) async -> Bool {
guard balance >= amount else { return false }
await Task.sleep(nanoseconds: 1_000_000) // simulate slow operation
balance -= amount // CAREFUL: state may have changed during await
return true
}
}
// Issue: between guard and assignment, another call to withdraw might have run.
// Fix: re-check invariants after await, or wrap in non-suspending code.
// 10) Actor isolation in protocols
protocol DataSource: Sendable {
func fetch() async -> [String]
}
actor RemoteDataSource: DataSource {
func fetch() async -> [String] { ["a", "b"] }
}
// Conforming actor methods are async; nonisolated for protocol compatibility when stateless.
// 11) Nonisolated init
actor Service {
let id = UUID() // nonisolated implicitly (let in init)
private var connections = 0
}
// init is treated specially; access to actor-isolated members after init is fine.
// 12) Distributed actors (Swift 5.7+)
// import Distributed
// distributed actor Account {
// distributed func balance() async throws -> Double { /* ... */ 0 }
// }
// Used for distributed systems; same syntax, but methods cross network boundaries.
// 13) Migrating from class + DispatchQueue
// Old pattern:
class OldCache {
private let queue = DispatchQueue(label: "cache.queue")
private var data = [String: String]()
func set(_ key: String, _ value: String) {
queue.async { self.data[key] = value }
}
func get(_ key: String, completion: @escaping (String?) -> Void) {
queue.async { completion(self.data[key]) }
}
}
// New (actor):
actor NewCache {
private var data = [String: String]()
func set(_ key: String, _ value: String) { data[key] = value }
func get(_ key: String) -> String? { data[key] }
}
// No queue, no completion handlers, no escaping, compiler-checked safety.
// 14) Performance notes
// • Actor method calls have overhead (queue + context switch)
// • For hot paths, batch operations inside the actor
// • Use let constants + nonisolated to avoid round-trips
// • Sendable struct values are CHEAPER than actor methods
// 15) Common bugs
// • Forgetting await on cross-actor calls → compile error
// • Mutating UI state from off the main actor → 'main actor-isolated' compile error; mark @MainActor
// • Treating actors as locks for one operation — actors serialise EVERYTHING; bottleneck
// • Reentrancy bugs — state changes during await; re-check invariants
// • Trying to access actor's private members from outside — only via methods
// • Sendable conformance for class types — needs @unchecked Sendable + manual proof
// • Conforming to non-Sendable protocols inside an actor — compiler warns
// • Holding actor reference across long-lived closures — retain cycle / leak
// • Cancellation not propagated — use Task.checkCancellation inside actor methods
// • Global actor confused with @MainActor — every global actor is independent
Why it matters
Actors serialise state access to prevent data races; cross-actor calls cost an await. Use @MainActor for view models and UI state, nonisolated for immutable members, and global actors when several types must share an executor. Watch reentrancy — state can change during await, so re-check invariants after suspending.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
actor Counter {
private var n = 0
func bump() { n += 1 }
func value() -> Int { n }
}
let c = Counter()
Task { await c.bump() }
Try it Yourself »
Discussion
Loading…