In the session Discover concurrency in SwiftUI (at 19:48), the following sample code is presented as an example of starting an async task from a Button action (which is synchronous):
struct SavePhotoButton: View {
var photo: SpacePhoto
@State private var isSaving = false
var body: some View {
Button {
Task {
isSaving = true
await photo.save()
isSaving = false
}
} label: {
Text("Save")
// …
}
// …
}
}
(The code on the slide uses async { … }
. I replaced this with the current Task { … }
syntax.)
I'm wondering if manipulating view state from inside the task closure like this is allowed. In fact, when you compile this with -Xfrontend -warn-concurrency
, you get compiler warnings on all three lines in the task closure:
Task {
// warning: Cannot use parameter 'self' with a non-sendable type 'SavePhotoButton' from concurrently-executed code
isSaving = true
// same warning
await photo.save()
// same warning
isSaving = false
}
You have to mark the view as @MainActor
to get rid of the warnings:
@MainActor
struct SavePhotoButton: View { … }
Questions:
-
Can you confirm that the sample code is invalid without the
@MainActor
annotation on the view? -
How does the
Task { … }
closure guarantees that it runs on the main actor. I know thatTask { … }
inherits the current actor execution context, but how does that work here?My guess:
View.body
is annotated with@MainActor
in the SwiftUI module interface- Actor context inheritance is based on the lexical scope, so the fact that the
Task { … }
closure is insidebody
is enough for it to inherit that context, even if it's called from another context.
Is this correct?
Thanks.