← thecodex.expert · The Codex Family of Knowledge
Swift

Swift Concurrency Model

Reading a plain property on an actor from outside it genuinely requires await — not because the read is slow, but because the actor might be busy handling another call, and the compiler won’t let that ambiguity become a silent data race.

Swift 6 Even a property read needs await Last verified:
Canonical Definition

Declared with actor instead of class, an actor's methods and properties are automatically actor-isolated — code OUTSIDE the actor must use await to access them, even for what looks like a simple property read, since the actor might currently be handling a different call and needs to serialize access to prevent two tasks from mutating its state simultaneously. This is enforced by the compiler, not left to convention, structurally eliminating the exact class of data race that the same mutable state inside an ordinary class could produce. @MainActor is a special, built-in global actor bound specifically to the main thread, and marking a type or function @MainActor guarantees its code always runs there — essential for UI code, which must never be touched from a background thread.

Declaring an actor

Even balance, a simple MUTABLE stored property (var), requires await from outside the actor — genuinely different from a class, where the same property read would need no await at all. The one exception: a constant (let) stored property can be read from outside the actor with no await, since it can never change and so creates no risk of a data race.

Swiftbasic_actor.swift
actor BankAccount {
    private var balance: Double = 0

    func deposit(_ amount: Double) {
        balance += amount
    }

    func getBalance() -> Double {
        return balance
    }
}

let account = BankAccount()
await account.deposit(100)              // await required — even for calling a method
let current = await account.getBalance()   // await required — even for a simple read

Why this structurally prevents data races

If two tasks call deposit concurrently, the actor serializes them — one completes fully before the other begins its access to balance, which is exactly the guarantee an ordinary class calling the same code from two threads simultaneously would NOT have.

Swiftconcurrent_safety.swift
async let d1: Void = account.deposit(50)
async let d2: Void = account.deposit(75)
_ = await [d1, d2]   // both run concurrently, but the ACTOR serializes actual access to balance
// no data race possible — the actor guarantees this structurally, not by convention

@MainActor: guaranteed main-thread execution

Marking a class @MainActor means every one of its methods automatically runs on the main thread — exactly the guarantee UI code needs, enforced by the compiler rather than left to the programmer remembering to dispatch correctly.

Swiftmain_actor.swift
@MainActor
class ViewModel {
    var title: String = "Loading..."   // always safely updated on the main thread
}

Sources

1
Apple / Swift.org. The Swift Programming Language, "Concurrency — Actors," docs.swift.org/swift-book.