SwiftUI’s .job
modifier inherits its actor context from the encircling perform. In the event you name .job
inside a view’s physique
property, the async operation will run on the principle actor as a result of View.physique
is (semi-secretly) annotated with @MainActor
. Nonetheless, in the event you name .job
from a helper property or perform that isn’t @MainActor
-annotated, the async operation will run within the cooperative thread pool.
Right here’s an instance. Discover the 2 .job
modifiers in physique
and helperView
. The code is an identical in each, but solely certainly one of them compiles — in helperView
, the decision to a main-actor-isolated perform fails as a result of we’re not on the principle actor in that context:
import SwiftUI
@MainActor func onMainActor() {
print("on MainActor")
}
struct ContentView: View {
var physique: some View {
VStack {
helperView
Textual content("in physique")
.job {
// We are able to name a @MainActor func with out await
onMainActor()
}
}
}
var helperView: some View {
Textual content("in helperView")
.job {
// ?? Error: Expression is 'async' however shouldn't be marked with 'await'
onMainActor()
}
}
}
This habits is brought on by two (semi-)hidden annotations within the SwiftUI framework:
-
The
View
protocol annotates itsphysique
property with@MainActor
. This transfers to all conforming varieties. -
View.job
annotates itsmotion
parameter with@_inheritActorContext
, inflicting it to undertake the actor context from its use website.
Sadly, none of those annotations are seen within the SwiftUI documentation, making it very obscure what’s happening. The @MainActor
annotation on View.physique
is current in Xcode’s generated Swift interface for SwiftUI (Leap to Definition of View
), however that characteristic doesn’t work reliably for me, and as we’ll see, it doesn’t present the entire reality, both.
To essentially see the declarations the compiler sees, we have to take a look at SwiftUI’s module interface file. A module interface is sort of a header file for Swift modules. It lists the module’s public declarations and even the implementations of inlinable features. Module interfaces use regular Swift syntax and have the .swiftinterface
file extension.
SwiftUI’s module interface is situated at:
[Path to Xcode.app]/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/SwiftUI.framework/Modules/SwiftUI.swiftmodule/arm64e-apple-ios.swiftinterface
(There may be a number of .swiftinterface
recordsdata in that listing, one per CPU structure. Decide any certainly one of them. Professional tip for viewing the file in Xcode: Editor > Syntax Coloring > Swift permits syntax highlighting.)
Inside, you’ll discover that View.physique
has the @MainActor(unsafe)
attribute:
@out there(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@_typeEraser(AnyView) public protocol View {
// …
@SwiftUI.ViewBuilder @_Concurrency.MainActor(unsafe) var physique: Self.Physique { get }
}
And also you’ll discover this declaration for .job
, together with the @_inheritActorContext
attribute:
@out there(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 job(
precedence: _Concurrency.TaskPriority = .userInitiated,
@_inheritActorContext _ motion: @escaping @Sendable () async -> Swift.Void
) -> some SwiftUI.View {
modifier(_TaskModifier(precedence: precedence, motion: motion))
}
#endif
// …
}
Armed with this data, every thing makes extra sense:
- When used inside
physique
,job
inherits the@MainActor
context fromphysique
. - When used outdoors of
physique
, there isn’t any implicit@MainActor
annotation, sojob
will run its operation on the cooperative thread pool by default. (Except the view incorporates an@ObservedObject
or@StateObject
property, which someway makes your entire view@MainActor
. However that’s a special matter.)
The lesson: in the event you use helper properties or features in your view, take into account annotating them with @MainActor
to get the identical semantics as physique
.
By the way in which, word that the actor context solely applies to code that’s positioned immediately contained in the async closure, in addition to to synchronous features the closure calls. Async features select their very own execution context, so any name to an async perform can change to a special executor. For instance, in the event you name URLSession.knowledge(from:)
inside a main-actor-annotated perform, the runtime will hop to the worldwide cooperative executor to execute that methodology. See SE-0338: Make clear the Execution of Non-Actor-Remoted Async Capabilities for the exact guidelines.
I perceive Apple’s impetus to not present unofficial API or language options within the documentation lest builders get the preposterous concept to make use of these options in their very own code!
Nevertheless it makes understanding so a lot tougher. Earlier than I noticed the annotations within the .swiftinterface
file, the habits of the code firstly of this text by no means made sense to me. Hiding the main points makes issues appear to be magic after they truly aren’t. And that’s not good, both.