How @MainActor preservation rule works?

I haven't checked SwiftUI version within the compiler, but more likely you are experiencing that due to the Button's action not being annotated to run on main actor. With strict concurrency checks there should be a warning that passing closure looses main actor isolation. It is widely adopted due to that (and other reasons) in SwiftUI cases to always annotate views with main actor as whole, that eliminates most of the issues you might face with concurrency right now.

First issue in that example of non-SwiftUI cases that all (except main-actor annotated, but right now it better to add there as well) closures has to specify that closure is @Sendable - in each case you have closure crossing isolation boundary. And with strict concurrency checks Swift will point you that (as warnings for now).

Once you annotate sendability of closures, you will get new warning in test cases, specifically for the test_Not_Preserved:

@MainActor
func test_Not_Preserved() async {
    let exp = expectation(description: #function)
    let action = { @MainActor @Sendable in
        XCTAssertTrue(Thread.isMainThread)
        exp.fulfill()
    }
    
    let sut = Not_Preserved(action: { action() })  // error: call to main actor-isolated let `action` in a synchronous nonisolated context

    sut.run()

    await fulfillment(of: [exp])
}

Which has a lot of sense now - Not_Preserved struct is nonisolated, so all the invocations as well.

Once such sendability warnings will become an error, it would be impossible to write such code. Yet currently that is a bit unpleasant behaviour.

All of that was summarized by this answer - Even if you assign the `@MainActor` attribute to a closure, the processing inside the closure is not executed on the main thread - #10 by John_McCall

1 Like