iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…