SwiftUI @Environment* accessibility in View init()

Hi,

I want to use @EnvironmentObject or @Environment to initialise a view model, see below:

struct ExampleView: View {
    @EnvironmentObject
    private var envData: EnvironmentData
    
    @ObservedObject
    var exampleViewModel: ExampleViewModel
    
    init() {
        exampleViewModel = ExampleViewModel(envData: envData)
    }
 
    var body: some View {
        Text(exampleViewModel.value)
    }
}

Currently, this is not possible. To work around this limitation i am using the following approach:

struct ExampleView: View {

    struct Content: View {
        @ObservedObject
        var exampleViewModel: ExampleViewModel
       
        init(envData: EnvironmentData) {
            exampleViewModel = ExampleViewModel(envData: envData)
        }
    
        var body: some View {
            Text(exampleViewModel.value)
        }
    }
    
    @EnvironmentObject
    private var envData: EnvironmentData
 
    var body: some View {
        Content(envData: envData)
    }
}

This adds a lot of boilerplate code - especially when a view has multiple parameters. Is there a better way?

It would be great if you could use syntax similar to the following, and inject environment values using init parameters with a parameter attribute.

struct ExampleView: View {
    @ObservedObject
    var exampleViewModel: ExampleViewModel

    init(@EnvironmentObject envData: EnvironmentData) {
        exampleViewModel = ExampleViewModel(envData: envData)
    }
    
    var body: some View {
        Text(exampleViewModel.value)
    }
}

At the very least, access to a read-only environment init parameter or prop on the view would solve this problem.

Given SwiftUI architecture fits naturally with MVVM, i'd think this would be a common usage pattern. If there isn't a better solution, maybe a pitch could be created to make the environment (and probably @ObservableObject) accessible from the view initialiser.

Interested in feedback, Adam.

Another alternative would be to send in envData from parent, and have parents holding envData.

PS

What does Published do in View? Should it be State or ObservedObject?

It should be @ObservedObject - typo.

Does exampleViewModel change at all if envData is not changing?

PS

You can edit the post, to fix the typo.

By "change" do you mean state changes? If so, then yes - the view model can @Publish state changes that do not originate from envData.

This is nit a limitation but correct by design. If you would push the environment into your model and pass down the model to some view down the view graph, you would be likely to get inconsistency bugs.

2 Likes

I think I understand why this is the case. The idea is that initializing a view struct doesn't mean that it's actually going to be presented on-screen any time soon - there is no environment at all yet. The environment is passed when body is called, so it can't be read til then.

A workaround I've found is to pull the view apart into two structs.

So if you wanna do this:

struct OnlyView: View {
    @Environment
    var fooBar: FooBar

    init() {
        use(fooBar)
    }

    var body: some View {
        Text(fooBar.description)
    }
}

Then structure it like this:

struct Parent: View {
    @Environment
    var fooBar: FooBar

    var body: some View {
        Child(fooBar: $fooBar)
    }
}


struct Child: View {
    @Binding
    var fooBar: FooBar

    init(fooBar: Binding<FooBar>) {
        self._fooBar = fooBar
        use(fooBar)
    }

    var body: some View {
        Text(fooBar.description)
    }
}

@ BenLeggiero simple and good solution

1 Like