SwiftUI’s .activity
modifier inherits its actor context from the encircling operate. For those who name .activity
inside a view’s physique
property, the async operation will run on the primary actor as a result of View.physique
is (semi-secretly) annotated with @MainActor
. Nevertheless, in case you name .activity
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 .activity
modifiers in physique
and helperView
. The code is equivalent in each, but solely one in all them compiles — in helperView
, the decision to a main-actor-isolated operate fails as a result of we’re not on the primary 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")
.activity {
// We will name a @MainActor func with out await
onMainActor()
}
}
}
var helperView: some View {
Textual content("in helperView")
.activity {
// ❗️ 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.activity
annotates itsmotion
parameter 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 occurring. 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.
To essentially 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 capabilities. 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 will be a number of .swiftinterface
information in that listing, one per CPU structure. Choose any one in all 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:
@accessible(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 .activity
, together with the @_inheritActorContext
attribute:
@accessible(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 activity(
precedence: _Concurrency.TaskPriority = .userInitiated,
@_inheritActorContext _ motion: @escaping @Sendable () async -> Swift.Void
) -> some SwiftUI.View {
modifier(_TaskModifier(precedence: precedence, motion: motion))
}
#endif
// …
}
Armed with this information, every little thing makes extra sense:
- When used inside
physique
,activity
inherits the@MainActor
context fromphysique
. - When used exterior of
physique
, there isn’t any implicit@MainActor
annotation, soactivity
will run its operation on the cooperative thread pool by default. -
Until the view comprises an
@ObservedObject
or@StateObject
property, which makes your complete view@MainActor
by way of this obscure rule for property wrappers whosewrappedValue
property is sure to a worldwide actor:A struct or class containing a wrapped occasion property with a worldwide actor-qualified
wrappedValue
infers actor isolation from that property wrapperReplace Could 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 easier. Within the Swift 5 language mode, you’ll be able to decide into the higher habits with the
-enable-upcoming-feature
DisableOutwardActorInference
compiler flags. I like to recommend you do.
The lesson: in case you use helper properties or capabilities in your view, think about annotating them with @MainActor
to get the identical semantics as physique
.
By the best way, be aware that the actor context solely applies to code that’s positioned immediately contained in the async closure, in addition to to synchronous capabilities the closure calls. Async capabilities select their very own execution context, so any name to an async operate can swap to a unique executor. For instance, in case 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!
But it surely makes understanding so a lot tougher. Earlier than I noticed the annotations within the .swiftinterface
file, the habits of the code at first of this text by no means made sense to me. Hiding the main points makes issues appear to be magic once they truly aren’t. And that’s not good, both.