15.4 C
Canberra
Friday, September 20, 2024

The place View.activity will get its main-actor isolation from – Ole Begemann


SwiftUI’s .activity modifier inherits its actor context from the encircling perform. In case you name .activity 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, in case you name .activity 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 .activity modifiers in physique and helperView. The code is an identical in each, but solely considered 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:


The place View.activity will get its main-actor isolation from – Ole Begemann
We are able to name a main-actor-isolated perform from inside 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 are able to name a @MainActor func with out await
          onMainActor()
        }
    }
  }

  var helperView: some View {
    Textual content("in helperView")
      .activity {
        // ❗️ Error: Expression is 'async' however just isn't marked with 'await'
        onMainActor()
      }
  }
}

This conduct is attributable to two (semi-)hidden annotations within the SwiftUI framework:

  1. The View protocol annotates its physique property with @MainActor. This transfers to all conforming varieties.

  2. View.activity annotates its motion 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 occurring. The @MainActor annotation on View.physique is current in Xcode’s generated Swift interface for SwiftUI (Leap to Definition of View), however that function doesn’t work reliably for me, and as we’ll see, it doesn’t present the entire reality, both.


Xcode showing the generated interface for SwiftUI’s View protocol. The @MainActor annotation on View.body is selected.
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 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 could be a number of .swiftinterface recordsdata in that listing, one per CPU structure. Decide any considered 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 .activity, 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 activity(
      precedence: _Concurrency.TaskPriority = .userInitiated,
      @_inheritActorContext _ motion: @escaping @Sendable () async -> Swift.Void
    ) -> some SwiftUI.View {
      modifier(_TaskModifier(precedence: precedence, motion: motion))
    }
  #endif
  // …
}

Xcode showing the declaration for the View.task method in the SwiftUI.swiftinterface file. The @_inheritActorContext annotation is selected.
SwiftUI’s module interface file exhibits the @_inheritActorContext annotatation on View.activity.

Armed with this information, every little thing makes extra sense:

  • When used inside physique, activity inherits the @MainActor context from physique.
  • When used exterior of physique, there isn’t a implicit @MainActor annotation, so activity will run its operation on the cooperative thread pool by default.
  • Until the view incorporates an @ObservedObject or @StateObject property, which makes the complete view @MainActor through this obscure rule for property wrappers whose wrappedValue property is sure to a world actor:

    A struct or class containing a wrapped occasion property with a world actor-qualified wrappedValue infers actor isolation from that property wrapper

    Replace Could 1, 2024: SE-0401: Take away Actor Isolation Inference attributable to Property Wrappers removes the above rule when compiling in Swift 6 language mode. This can be a good change as a result of it makes reasoning about actor isolation easier. Within the Swift 5 language mode, you may choose into the higher conduct 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 way in which, word that the actor context solely applies to code that’s positioned instantly 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 perform can change to a special executor. For instance, in case you name URLSession.information(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 conduct of the code at first of this text by no means made sense to me. Hiding the small print makes issues look like magic once they truly aren’t. And that’s not good, both.

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

[td_block_social_counter facebook="tagdiv" twitter="tagdivofficial" youtube="tagdiv" style="style8 td-social-boxed td-social-font-icons" tdc_css="eyJhbGwiOnsibWFyZ2luLWJvdHRvbSI6IjM4IiwiZGlzcGxheSI6IiJ9LCJwb3J0cmFpdCI6eyJtYXJnaW4tYm90dG9tIjoiMzAiLCJkaXNwbGF5IjoiIn0sInBvcnRyYWl0X21heF93aWR0aCI6MTAxOCwicG9ydHJhaXRfbWluX3dpZHRoIjo3Njh9" custom_title="Stay Connected" block_template_id="td_block_template_8" f_header_font_family="712" f_header_font_transform="uppercase" f_header_font_weight="500" f_header_font_size="17" border_color="#dd3333"]
- Advertisement -spot_img

Latest Articles