Bonton255
Bonton255

Reputation: 2419

SwiftUI: @Environment not receiving provided value down the view hierarchy

I am following the example of this project to create my iOS app (thanks Alexey!), but can't get the @Environment variable to receive the value that is being passed down the UI hierarchy. The top level view receives the correct value, but the downstream view receives the default value.

EDIT: After tying to replicate Asperi's code, I found that this behavior happens only when the downstream view is invoked via a NavigationLink. Updated the code below:

EDIT2: The problem was with where the environment method was being invoked. Invoking it on the NavigationView instead of the MainView solved the problem. Code updated below:

Custom Environment key - DIContainer

struct DIContainer: EnvironmentKey {
    
    let interactor: Interactor
    
    init(interactor: Interactor) {
        self.interactor = interactor
    }
    
    static var defaultValue: Self { Self.default }
    
    private static let `default` = Self(interactor: .stub)
}

extension EnvironmentValues {
    var injected: DIContainer {
        get { self[DIContainer.self] }
        set { self[DIContainer.self] = newValue }
    }
}

App struct

private let container: DIContainer

init() {
    container = DIContainer(interactor: RealInteractor())
}

var body: some Scene {
    WindowGroup {
        NavigationView {
            MainView()
        }
        .environment(\.injected, container)
}

Main View

struct MainView: View {

    @Environment(\.injected) private var injected: DIContainer
    // `injected` has the `RealInteractor`, as expected

    var body: some View {
        VStack {
            Text("Main: \(injected.foo())")  \\ << Prints REAL

            NavigationLink(destination: SearchView()) {
                Text("Search")
            }
        }
    }
}

Search View

struct SearchView: View {

    @Environment(\.injected) private var injected: DIContainer
    // `injected` has the `StubInteractor`, why?

    var body: some View {
        Text("Search: \(injected.foo())")
    }
}

I am able to solve this problem by modifying the MainView like so:

    var body: some View {
        SearchView()
            .environment(\.injected, container)
    }

But isn't avoiding doing this repeatedly the purpose of @Environment?

Any guidance/pointers appreciated.

Upvotes: 1

Views: 694

Answers (1)

Asperi
Asperi

Reputation: 257711

I've tryied to replicate all parts and to make them compiled... and the result just works as expected - environment is passed down the view hierarchy, so you might miss something in your real code.

Here is complete module, tested with Xcode 12.4 / iOS 14.4

class Interactor {                    // << replicated !!
    static let stub = Interactor()
    func foo() -> String { "stub" }
}

class RealInteractor: Interactor {             // << replicated !!
    override func foo() -> String { "real" }
}

struct ContentView: View {                  // << replicated !!
    private let container: DIContainer

    init() {
        container = DIContainer(interactor: RealInteractor())
    }

    var body: some View {
        NavigationView {
            MainView()
        }
        .environment(\.injected, container) // << to affect any links !!
    }
}

// no changes in env parts
struct DIContainer: EnvironmentKey {

    let interactor: Interactor

    init(interactor: Interactor) {
        self.interactor = interactor
    }

    static var defaultValue: Self { Self.default }

    private static let `default` = Self(interactor: .stub)
}

extension EnvironmentValues {
    var injected: DIContainer {
        get { self[DIContainer.self] }
        set { self[DIContainer.self] = newValue }
    }
}

struct MainView: View {

    @Environment(\.injected) private var injected: DIContainer
    // `injected` has the `RealInteractor`, as expected

    var body: some View {
        SearchView()
    }
}


// just tested here
struct SearchView: View {

    @Environment(\.injected) private var injected: DIContainer

    var body: some View {
        Text("Result: \(injected.interactor.foo())")     // << works fine !!
    }
}

demo

Upvotes: 1

Related Questions