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> cacheRemember
- Actor state is accessed one task at a time; outside callers
await @MainActorputs 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
awaitinside one of its methods.