Where View.task Gets Its Main-actor Isolation From
SwiftUI’s
.task
modifier inherits its actor context from the surrounding function. If you call.task
inside a view’sbody
property, the async operation will run on the main actor becauseView.body
is (semi-secretly) annotated with@MainActor
. However, if you call.task
from a helper property or function that isn’t@MainActor
-annotated, the async operation will run in the cooperative thread pool.[…]
The
View
protocol annotates itsbody
property with@MainActor
. This transfers to all conforming types.
View.task
annotates itsaction
parameter with@_inheritActorContext
, causing it to adopt the actor context from its use site.Sadly, none of these annotations are visible in the SwiftUI documentation, making it very difficult to understand what’s going on. […] To really see the declarations the compiler sees, we need to look at SwiftUI’s module interface file.
[…]
When used outside of
body
, there is no implicit@MainActor
annotation, sotask
will run its operation on the cooperative thread pool by default. (Unless the view contains an@ObservedObject
or@StateObject
property, which somehow makes the entire view@MainActor
. But that’s a different topic.)
Swift concurrency is a mess.
Every software framework that tries to automagically do “the right thing” is doomed to break your code in the most unexpected way.
Add a new member variable and it changes execution of unrelated parts from concurrent to serial. How wild is this?
Currently, we can infer
@MainActor
on an entire type based on the presence of certain property wrappers within that type. TL;DR: I’m making a case that that’s a bad idea, and we should reconsider it if possible.
Previously:
- How the Swift Compiler Knows That DispatchQueue.main Implies @MainActor
- The Power of SwiftUI “task” View Modifier
- @MainActor Not Guaranteed