Where View.task Gets Its Main-actor Isolation From
SwiftUI’s
.taskmodifier inherits its actor context from the surrounding function. If you call.taskinside a view’sbodyproperty, the async operation will run on the main actor becauseView.bodyis (semi-secretly) annotated with@MainActor. However, if you call.taskfrom a helper property or function that isn’t@MainActor-annotated, the async operation will run in the cooperative thread pool.[…]
The
Viewprotocol annotates itsbodyproperty with@MainActor. This transfers to all conforming types.
View.taskannotates itsactionparameter 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@MainActorannotation, sotaskwill run its operation on the cooperative thread pool by default. (Unless the view contains an@ObservedObjector@StateObjectproperty, 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
@MainActoron 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