Making View's init nonisolated with environment variables

Views with custom nonisolated init fail to compile when using @MainActor-isolated @Environment properties (e.g., \.openURL, \.dismiss). From the Swift 6 migration documentation, it seems encouraged to define inits nonisolated, and it seems base SwiftUI components also have their inits defined as nonisolated (e.g. TimelineView, LazyHStack, etc), which makes sense.

How do you achieve marking a SwiftUI View's init as nonisolated when it uses environment variables, without producing the following build error "Main actor-isolated default value of 'self.openURL' cannot be used in a nonisolated initalizer" ?

It seems @State variable defined in a view has the ability to be set in a nonisolated init but not @Environment. What mechanism prevents this under the hood ?

Answered by Frameworks Engineer in 892239022

Great question! In SwiftUI, we have made it as easy as possible to use it in the Swift 6 language mode. You should not need to define your custom Views initializers as nonisolated, because the expectation is that they will be initialized from the parent view's isolation context, and View protocol is @MainActor in SwiftUI. This means that there is really no reason for your view to be initialized from a nonisolated context, since the context will be main-actor-isolated anyway.

Great observation that SwiftUI's own custom Views have their initializer marked as nonisolated. This is a pattern that lets us have more flexibility internally in the framework, but in terms of API, we rely on the fact that these views will be automatically isolated to the @MainActor when used in View.body. While this is a pattern we use internally, it is not needed to replicate for your own custom components.

I hope this answers your question! If not, we'd love to see the sample code where it is tricky to avoid marking View.init as nonisolated.

Thank you!

You don't need nonisolated init for a view.

Accepted Answer

Great question! In SwiftUI, we have made it as easy as possible to use it in the Swift 6 language mode. You should not need to define your custom Views initializers as nonisolated, because the expectation is that they will be initialized from the parent view's isolation context, and View protocol is @MainActor in SwiftUI. This means that there is really no reason for your view to be initialized from a nonisolated context, since the context will be main-actor-isolated anyway.

Great observation that SwiftUI's own custom Views have their initializer marked as nonisolated. This is a pattern that lets us have more flexibility internally in the framework, but in terms of API, we rely on the fact that these views will be automatically isolated to the @MainActor when used in View.body. While this is a pattern we use internally, it is not needed to replicate for your own custom components.

I hope this answers your question! If not, we'd love to see the sample code where it is tricky to avoid marking View.init as nonisolated.

Thank you!

I'd need nonisolated because I am registering my view using Swinject from a nonisolated context.

I also had the case where a developer was doing navigation updates (@MainActor operation) in the init, which was technically possible as the init is @MainActor.

What is the technical limitation that enables setting a @State var value in a nonisolated context and that prevents @Environment from being set there ?

I'd need nonisolated because I am registering my view using Swinject from a nonisolated context.

Is it possible to make this context @MainActor-isolated?

The concurrency requirement for a framework like SwiftUI is different than an application. SwiftUI's API has to be compatible with a vast amount of use cases, including apps building in Swift 5 mode, apps using older SDKs where compiler diagnostic with regard to concurrency behaves differently, etc. Nonisolated inits is necessary to maintain source compatibility with all these edge use cases.

Unless you are working on a framework that needs to be source/ABI compatible with older compilers, an unknown number of concurrency setting combinations, etc (in which case, bravo!), you really shouldn't have the burden of maintaining nonisolated initializers.

It's a little surprising that you need to dependency inject a view. Usually dependency injection deals with data flow. I'd consider using other method to perhaps modularize your view code. For example, if your view isn't changing frequently, you can remove dependencies between views using AnyView.

Indeed injecting Views is something particular. I will go back thinking about this.

Replace @StateObject with @ObservedObject.

Making View's init nonisolated with environment variables
 
 
Q