Which actor do a view’s body, its task closures and its models run on in Swift 6, and what does the compiler enforce?
Predict first
Does mutating a main-actor model from a nonisolated function compile?
import SwiftUI
@MainActor @Observable final class Model { var n = 0 }
func bump(_ m: Model) { m.n += 1 }
- No: the property is main-actor isolated
- Yes
- Only with @Sendable
Predict first
And a view whose task mutates the model?
import SwiftUI
@MainActor @Observable final class Model { var n = 0 }
struct V: View {
@State private var model = Model()
var body: some View { Text("\(model.n)").task { model.n += 1 } }
}
- It compiles
- It fails: task is nonisolated
- It crashes
Mental model. The main actor is the stage: the compiler checks that your own props (models) are only used on stage, while SwiftUI promises to call the show on stage.
SwiftUI calls body on the main thread and runs the closures you give .task, onChange and buttons on the main actor, so touching view state from them is fine; the second check, a view whose .task mutates a @MainActor model, compiles in Swift 6 mode. What the compiler enforces is isolation on your own types: the first check calls a function that mutates a @MainActor @Observable model from ordinary (nonisolated) code and fails to compile. I tried to show the same failure for a view’s body by calling V().body from a nonisolated function, and it compiled against the iOS 17, 18 and 26 deployment targets with the iOS 27 SDK, so I will not claim the compiler isolates body in every configuration; treat the main-actor rule for views as a runtime contract plus the isolation you declare on models.
Practical rules. Annotate view models that drive UI with @MainActor (an @Observable class is not isolated by itself), so mistakes become compile errors instead of races. Put heavy work in a nonisolated async function or an actor, await it from .task, and assign the result back on the main actor. Do not capture view or model state in a Task.detached or a background queue; copy the value first. If Swift 6 reports a Sendable capture in a view, the fix is usually to move the logic into an isolated model rather than add @unchecked Sendable. Whether a .task body’s synchronous part runs before the first frame is not guaranteed.
Declared isolation is enforced in Swift 6 mode.
View closures are main-actor contexts.
Common traps
- Assuming
@Observablemakes a model main-actor. Annotate it yourself. - Silencing the compiler with
@unchecked Sendable. Move the work out of the view instead.
They might follow up with
- Where does awaited work run? On whatever executor the awaited function declares; the continuation returns to the main actor.
- Does Swift 5 mode enforce this? It warns at most; Swift 6 mode makes it an error.
Related questions
- 8.3.3 How does the Observation framework differ from ObservableObject, and how does tracking work?
- 8.4.1 When do onAppear, task and onChange run, and which one do you use for async work?
- 8.11.1 Trace one view through its first render and a value change: what is logged, in what order, and what about an…
← Trace one view through its first render and a value change: what is…