SwiftUI’s .process modifier inherits its actor context from the encompassing operate. If you happen to name .process 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. Nevertheless, when you name .process from a helper property or operate that isn’t @MainActor-annotated, the async operation will run within the cooperative thread pool.
Right here’s an instance. Discover the 2 .process modifiers in physique and helperView. The code is an identical in each, but solely one among them compiles — in helperView, the decision to a main-actor-isolated operate fails as a result of we’re not on the principle actor in that context:

physique, however not from a helper property.
import SwiftUI
@MainActor func onMainActor() {
print("on MainActor")
}
struct ContentView: View {
var physique: some View {
VStack {
helperView
Textual content("in physique")
.process {
// We are able to name a @MainActor func with out await
onMainActor()
}
}
}
var helperView: some View {
Textual content("in helperView")
.process {
// ❗️ Error: Expression is 'async' however just isn't marked with 'await'
onMainActor()
}
}
}
This habits is brought on by two (semi-)hidden annotations within the SwiftUI framework:
The
Viewprotocol annotates itsphysiqueproperty with@MainActor. This transfers to all conforming sorts.View.processannotates itsmotionparameter with@_inheritActorContext, inflicting it to undertake the actor context from its use web site.
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 (Bounce to Definition of View), however that function doesn’t work reliably for me, and as we’ll see, it doesn’t present the entire fact, both.

View.physique is annotated with @MainActor in Xcode’s generated interface for SwiftUI.
To actually see the declarations the compiler sees, we have to have 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 positioned 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 could be a number of .swiftinterface recordsdata in that listing, one per CPU structure. Decide any one among 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:
@obtainable(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 .process, together with the @_inheritActorContext attribute:
@obtainable(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 process(
precedence: _Concurrency.TaskPriority = .userInitiated,
@_inheritActorContext _ motion: @escaping @Sendable () async -> Swift.Void
) -> some SwiftUI.View {
modifier(_TaskModifier(precedence: precedence, motion: motion))
}
#endif
// …
}

@_inheritActorContext annotatation on View.process.
Armed with this data, the whole lot makes extra sense:
- When used inside
physique,processinherits the@MainActorcontext fromphysique. - When used outdoors of
physique, there is no such thing as a implicit@MainActorannotation, soprocesswill run its operation on the cooperative thread pool by default. Until the view comprises an
@ObservedObjector@StateObjectproperty, which makes your complete view@MainActorby way of this obscure rule for property wrappers whosewrappedValueproperty is certain to a world actor:A struct or class containing a wrapped occasion property with a world actor-qualified
wrappedValueinfers actor isolation from that property wrapperReplace Might 1, 2024: SE-0401: Take away Actor Isolation Inference brought on by Property Wrappers removes the above rule when compiling in Swift 6 language mode. It is a good change as a result of it makes reasoning about actor isolation less complicated. Within the Swift 5 language mode, you may choose into the higher habits with the
-enable-upcoming-featureDisableOutwardActorInferencecompiler flags. I like to recommend you do.
The lesson: when you use helper properties or features in your view, think about annotating them with @MainActor to get the identical semantics as physique.
By the way in which, notice that the actor context solely applies to code that’s positioned straight 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 operate can swap to a special executor. For instance, when you name URLSession.knowledge(from:) inside a main-actor-annotated operate, 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 Features 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 more durable. Earlier than I noticed the annotations within the .swiftinterface file, the habits of the code in the beginning of this text by no means made sense to me. Hiding the small print makes issues look like magic after they truly aren’t. And that’s not good, both.
