Actors and the main actor

Swift · Advanced · 14 min

Actors are Swift's built-in tool against data races, and @MainActor is how UI code stays on the main thread.

An actor is a reference type whose mutable state is protected: only one task at a time runs code isolated to the actor. From outside, accessing its properties or methods requires await, because you may have to wait your turn.

actor BankAccount {
    private var balance = 0
    func deposit(_ x: Int) { balance += x }
}
await account.deposit(10)

Inside the actor, code runs synchronously with direct access to its state. Mark a member nonisolated when it touches no mutable state (e.g. a let or a pure computed value), so callers don't need await.

Global actors isolate code that lives in many types to one shared executor. The important one is `@MainActor`, which runs on the main thread. Annotate view models, UI code and anything touching UIKit or AppKit with it. SwiftUI's View is main-actor isolated, and in Xcode 26 new app projects can default whole modules to the main actor.

Actors are reentrant: while an actor method is suspended at an await, other calls on the same actor can run. Invariants may change across an await, so re-check state afterwards and avoid splitting a read-modify-write around a suspension point.

Actors do not guarantee FIFO ordering of calls, and they don't make work faster — they serialize it. Keep actor methods short and move heavy pure computation out of them.

Example

actor ImageCache {
    private var store: [String: String] = [:]
    private(set) var hits = 0
    func image(for key: String) async -> String {
        if let cached = store[key] { hits += 1; return cached }
        let loaded = await download(key)     // suspension: actor may run other calls
        if let raced = store[key] { return raced }  // re-check after await
        store[key] = loaded
        return loaded
    }
    private func download(_ key: String) async -> String {
        try? await Task.sleep(for: .milliseconds(50))
        return "<\(key)>"
    }
    nonisolated var name: String { "cache" }   // no await needed
}
@MainActor final class ViewModel {
    var title = ""
    let cache = ImageCache()
    func load() async { title = await cache.image(for: "logo") }
}

let vm = ViewModel()          // top-level code runs on the main actor
await vm.load()
print(vm.title, vm.cache.name)                // <logo> cache

Remember

  • Actor state is accessed one task at a time; outside callers await
  • @MainActor puts UI code on the main thread
  • Actors are reentrant — re-check state after every await

Watch out for

  • Assuming nothing changed in the actor across an await inside one of its methods.

Check yourself

  1. Inside an actor method, you read `count`, then `await` a network call, then write `count + 1`. What is the risk?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/#Actors