Manipulating SwiftUI view state in a Task closure (button action)

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:

  1. Can you confirm that the sample code is invalid without the @MainActor annotation on the view?

  2. How does the Task { … } closure guarantees that it runs on the main actor. I know that Task { … } 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 inside body is enough for it to inherit that context, even if it's called from another context.

    Is this correct?

Thanks.

Manipulating SwiftUI view state in a Task closure (button action)
 
 
Q