What is the closure passed to `View.task(priority:_:)` implicitly using `@MainActor`

I noticed this when playing around with concurrency in Xcode 14. When using task in a view builder is allows me to call methods annotated with @MainActor but I can't figure out why. In attempt to see if it had something to do with the way task is defined as a method I tried to copy it's signature in my own method but it doesn't behave the same way.

@MainActor
func foo() {
    print("Hey!")
}

extension View {
    @inlinable func flask(priority: TaskPriority = .userInitiated, _ action: @escaping @Sendable () async -> Void) -> some View {
        return self
    }
}

struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
            .task {
                foo()
            }
            .flask {
                foo() // <-- Error: Expression is 'async' but is not marked with 'await'
            }
    }
}

I was expecting task to execute on a background thread given that you can pass a priority as it's first argument, but when I set a break point within the closure, it's indeed on the main thread. I guess for some reason it's adopting the @MainActor from body, and maybe the the priority is applied to async methods called within the task but not the task method itself.

How does this work?

I assume the parameter is annotated with @_inheritActorContext.

1 Like

Interesting. It's not documented anywhere I could find. It doesn't even show up if you cmd + click on task to see it's interface, but that's probably correct. Thanks, I didn't know about @_inheritActorContext.

Yeah, it's an underscored attribute so it won't show up in the IDE. You can find more information about it here - https://github.com/apple/swift/blob/main/docs/ReferenceGuides/UnderscoredAttributes.md#_inheritactorcontext

You can find it in SwiftUI's .swiftinterface file (aka the "header file" for Swift modules).

The module interface is located at:

[Path to Xcode.app]/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/SwiftUI.framework/Modules/SwiftUI.swiftmodule

Inside, you'll find this declaration for .task, including the @_inheritActorContext attribute:

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
extension SwiftUI.View {
  #if compiler(>=5.3) && $AsyncAwait && $Sendable && $InheritActorContext
  @inlinable public func task(priority: _Concurrency.TaskPriority = .userInitiated, @_inheritActorContext _ action: @escaping @Sendable () async -> Swift.Void) -> some SwiftUI.View {
        modifier(_TaskModifier(priority: priority, action: action))
    }
  
  #endif
  …
}

And you'll find that View.body has the @MainActor(unsafe) attribute:

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@_typeEraser(AnyView) public protocol View {
  …
  @SwiftUI.ViewBuilder @_Concurrency.MainActor(unsafe) var body: Self.Body { get }
}

The fact that View.body is @MainActor is significant because that's where .task inherits its actor context from.

At least if you use .task inside the body property, that is. If you place the .task modifier inside a helper property that isn't annotated with @MainActor, you'll see an error:

import SwiftUI

@MainActor
func foo() {
    print("Hey!")
}

struct ContentView: View {
    // is explicitly @MainActor via the View protocol
    var body: some View {
        helper
    }

    // is not @MainActor
    var helper: some View {
        Text("Hello, world!")
            .task {
                // Error: Expression is 'async' but is not marked with 'await'
                foo()
            }
    }
}
5 Likes