Chapter 11 — @MainActor and global actors
@MainActor is the actor that owns the main thread
@MainActor
final class ProfileViewModel { // the WHOLE type is main-actor isolated
var name = "" // all stored properties: main-actor
func refresh() { … } // all methods: main-actor
}
final class Mixed {
@MainActor var label: String = "" // just this property is main-actor
@MainActor func updateUI() { … } // just this method is main-actor
func compute() -> Int { … } // this one is not
}
// A closure can be main-actor isolated:
let onDone: @MainActor () -> Void = { updateLabel() }
flowchart LR
BG["background task"] -->|"await vm.refresh()"| HOP["hop to MAIN thread
(the @MainActor executor)"] HOP --> RUN["run refresh() on main"] RUN --> BACK["hop back"]
(the @MainActor executor)"] HOP --> RUN["run refresh() on main"] RUN --> BACK["hop back"]
Where @MainActor shows up in SwiftUI
Views are@MainActor. TheViewprotocol'sbodyis main-actor isolated, so everything you@Observableview models are usually@MainActor. Since a view model feeds the UI, marking it- The
AppandScenetypes are@MainActor. App startup runs on the main actor.
Getting onto the main actor from elsewhere
await viewModel.updateProgress(0.5) // viewModel is @MainActor; this hops to main
await MainActor.run {
progressLabel.text = "Halfway" // runs on main
}
// A delegate callback documented to run on the main thread, but not typed @MainActor:
func legacyDelegateDidFinish() {
MainActor.assumeIsolated {
self.label.text = "Done" // no await; we assert we're already on main
}
}
flowchart TB
Q{"Need to run on the main actor.
Where am I?"} Q -->|"Have a @MainActor method"| M1["await that method"] Q -->|"One-off closure, currently off main"| M2["await MainActor.run { }"] Q -->|"Already on main, compiler unaware"| M3["MainActor.assumeIsolated { }"]
Where am I?"} Q -->|"Have a @MainActor method"| M1["await that method"] Q -->|"One-off closure, currently off main"| M2["await MainActor.run { }"] Q -->|"Already on main, compiler unaware"| M3["MainActor.assumeIsolated { }"]
Building your own global actor
// Support/DatabaseActor.swift
@globalActor
actor DatabaseActor {
static let shared = DatabaseActor() // the required singleton instance
}
@DatabaseActor
final class QueryEngine {
func run(_ sql: String) -> [Row] { … } // always on the DatabaseActor
}
@DatabaseActor
func migrate() { … } // also on the DatabaseActor — serialized with QueryEngine
When to build one: when a subsystem (not a single object) needs serialized access — a database layer, an analytics pipeline, a hardware resource. When you just need to protect one object's state, a plain
actor(Chapter 9) is simpler and more focused. Don't reach for a global actor where a plain actor suffices; global isolation is a bigger hammer.
nonisolated inside a @MainActor type
@MainActor
final class ProfileViewModel {
let userID: UUID
var displayName = ""
init(userID: UUID) { self.userID = userID }
// Pure, touches only the immutable userID — no reason to require the main thread.
nonisolated var analyticsKey: String { "profile_\(userID)" }
}
What we built in this chapter
- Established
@MainActoras the global actor bound to the main thread — the mechanism for making - Saw why SwiftUI needs so few explicit annotations: **
Views,@Observableview models, andApp - Learned the three ways to get onto the main actor from elsewhere —
awaita@MainActormethod, - Built a custom global actor (
@globalActor+static let shared) to give a subsystem a single, - Used
nonisolatedinside a@MainActortype to keep pure helpers hop-free.
Mental model to take away
@MainActoris just an actor whose executor is the main thread. All the Chapter 9 rules apply:- SwiftUI puts you on the main actor by default (views, view models,
App), so the work is at the assumeIsolatedis an assertion, not an escape hatch — it traps if you're wrong. Use it only for- A global actor (
@globalActor) gives a whole subsystem one shared serialized domain; a plain