Hello,
I created examples to demonstrate the different results of @MainActor behaviour while passing async closures
First in SwiftUI views
struct RunActionView: View {
let title: String
let action: @MainActor () async -> Void
init(title: String, action: @MainActor @escaping () async -> Void) {
self.title = title
self.action = action
}
var body: some View {
Button(action: executeAction) {
Text(title)
}
}
private func executeAction() {
print(title)
Task { @MainActor in
try await run(operation: action)
}
}
}
struct RunActionWithThrowsView: View {
let title: String
let action: @MainActor () async throws -> Void
init(title: String, action: @MainActor @escaping () async throws -> Void) {
self.title = title
self.action = action
}
var body: some View {
Button(action: executeAction) {
Text(title)
}
}
private func executeAction() {
print(title)
Task { @MainActor in
try await run(operation: action)
}
}
}
func run(operation: () async throws -> Void) async throws {
try await operation()
}
struct RunActionDirectView: View {
let title: String
let action: @MainActor () async throws -> Void
init(title: String, action: @MainActor @escaping () async -> Void) {
self.title = title
self.action = action
}
var body: some View {
Button(action: executeAction) {
Text(title)
}
}
private func executeAction() {
print(title)
Task { @MainActor in
try await action()
}
}
}
struct RunActionWithTaskView: View {
let title: String
let action: @MainActor () async throws -> Void
init(title: String, action: @MainActor @escaping () async -> Void) {
self.title = title
self.action = action
}
var body: some View {
Button(action: executeAction) {
Text(title)
}
}
private func executeAction() {
print(title)
Task { @MainActor in
try await runWithTask(operation: action)
}
}
}
func runWithTask(operation: () async throws -> Void) async throws {
Task {}
try await operation()
}
struct ContainerView: View {
// When:
// * action: () async -> Void | All actions will be run on non MainActor
// * action: @Main () async -> Void | All actions will be run on MainActor
let action: () -> Void
var body: some View {
VStack {
RunActionView(title: "RunActionView", action: action) // TRUE
RunActionDirectView(title: "RunActionDirectView", action: action) // FALSE
RunActionWithTaskView(title: "RunActionWithTaskView", action: action) // FALSE
RunActionWithThrowsView(title: "RunActionWithThrowsView", action: action) // FALSE
}
}
}
Only in the first case the action will be run on the @MainActor
(when neither marked with async
nor @MainActor
.
But if I change as in the comments to async
in all cases the action will be run on non @MainActor
and in the most clear case when ContainerView.action
closure is marked as @MainActor
, then in all cases the action will be run on the @MainActor
.
Moreover in case of non SwiftUI's View
swift, we can observe again different behaviours:
struct Not_Preserved {
let action: () -> Void
func run() {
Async(action: action).run()
}
}
struct Preserved_When_Async {
let action: () async -> Void
func run() {
Async(action: action).run()
}
}
struct Preserved_When_Annotated {
let action: @MainActor () -> Void
func run() {
Async(action: action).run()
}
}
struct Async {
let action: () async -> Void
func run() {
Task { @MainActor in
await action()
}
}
}
final class PlaygroundTests: XCTestCase {
@MainActor
func test_Not_Preserved() async {
let exp = expectation(description: #function)
let action = { @MainActor in
XCTAssertTrue(Thread.isMainThread)
exp.fulfill()
}
let sut = Not_Preserved(action: action)
sut.run()
await fulfillment(of: [exp])
}
@MainActor
func test_Preserved_When_Async() async {
let exp = expectation(description: #function)
let action = { @MainActor in
XCTAssertTrue(Thread.isMainThread)
exp.fulfill()
}
let sut = Preserved_When_Async(action: action)
sut.run()
await fulfillment(of: [exp])
}
@MainActor
func test_Preserved_When_Annotated() async {
let exp = expectation(description: #function)
let action = { @MainActor in
XCTAssertTrue(Thread.isMainThread)
exp.fulfill()
}
let sut = Preserved_When_Annotated(action: action)
sut.run()
await fulfillment(of: [exp])
}
}
The link with the playground with the code above in zip.
Could someone explain the behaviour and what actually the @MainActor
annotation does in case of a closure?