NavigationStack not affected by EnvironmentObject changes
Asked Answered
D

1

6

I'm attempting to use @EnvironmentObject to pass an @Published navigation path into a SwiftUI NavigationStack using a simple wrapper ObservableObject, and the code builds without issue, but working with the @EnvironmentObject has no effect. Here's a simplified example that still exhibits the issue:

import SwiftUI

class NavigationCoordinator: ObservableObject {
    @Published var path = NavigationPath()

    func popToRoot() {
        path.removeLast(path.count)
    }
}

struct ContentView: View {
    @StateObject var navigationCoordinator = NavigationCoordinator()

    var body: some View {
        NavigationStack(path: $navigationCoordinator.path, root: {
            FirstView()
        })
            .environmentObject(navigationCoordinator)
    }
}

struct FirstView: View {
    var body: some View {
        VStack {
            NavigationLink(destination: SecondView()) {
                Text("Go To SecondView")
            }
        }
            .navigationTitle(Text("FirstView"))
    }
}

struct SecondView: View {
    var body: some View {
        VStack {
            NavigationLink(destination: ThirdView()) {
                Text("Go To ThirdView")
            }
        }
            .navigationTitle(Text("SecondView"))
    }
}

struct ThirdView: View {
    @EnvironmentObject var navigationCoordinator: NavigationCoordinator

    var body: some View {
        VStack {
            Button("Pop to FirstView") {
                navigationCoordinator.popToRoot()
            }
        }
            .navigationTitle(Text("ThirdView"))
    }
}

I am:

  • Passing the path into the NavigationStack path parameter
  • Sending the simple ObservableObject instance into the NavigationStack via the .environmentObject() modifier
  • Pushing a few simple child views onto the stack
  • Attempting to use the environment object in ThirdView
  • NOT crashing when attempting to use the environment object (e.g. "No ObservableObject of type NavigationCoordinator found")

Am I missing anything else that would prevent the deeply stacked view from using the EnvironmentObject to affect the NavigationStack's path? It seems like the NavigationStack just isn't respecting the bound path.

(iOS 16.0, Xcode 14.0)

Downstairs answered 14/9, 2022 at 22:23 Comment(0)
H
11

The reason your code is not working is that you haven't added anything to your path, so your path is empty. You can simply verify this by adding print(path.count) in your popToRoot method it will print 0 in the console.

To work with NavigationPath you need to use navigationDestination(for:destination:) ViewModifier, So for your example, you can try something like this.

ContentView:- Change NavigationStack like this.

NavigationStack(path: $navigationCoordinator.path) {
    VStack {
        NavigationLink(value: 1) {
            Text("Go To SecondView")
        }
    }
    .navigationDestination(for: Int.self) { i in
        if i == 1 {
            SecondView()
        }
        else {
            ThirdView()
        }
    }
}

SecondView:- Change NavigationLink like this.

NavigationLink(value: 2) {
    Text("Go To ThirdView")
}

This workaround works with Int but is not a better approach, so my suggestion is to use a custom Array as a path. Like this.

enum AppView {
    case second, third
}

class NavigationCoordinator: ObservableObject {
    @Published var path = [AppView]()
}

NavigationStack(path: $navigationCoordinator.path) {
    FirstView()
        .navigationDestination(for: AppView.self) { path in
            switch path {
            case .second: SecondView()
            case .third: ThirdView()
            }
        }
}

Now change NavigationLink in FirstView and SecondView like this.

NavigationLink(value: AppView.second) {
    Text("Go To SecondView")
}

NavigationLink(value: AppView.third) {
    Text("Go To ThirdView")
}

The benefit of the above is now you can use the button as well to push a new screen and just need to append in your path.

path.append(.second)
//OR
path.append(.third)

This will push a respected view.

For more details, you can read the Apple document of NavigationLink and NavigationPath.

Heidi answered 15/9, 2022 at 6:24 Comment(7)
Ah! I was under the mistaken impression that providing a path meant that pushing things onto the NavigationStack would automatically append them to the path.Downstairs
@CollinAllen It will if you go with NavigationLink(value:)Heidi
If I'm now using NavigationLink(value:), how do I now pass data from a parent to a child, say something fetched by SecondView to ThirdView? One benefit of using NavigationLink(destination:) was that the destination parameter could include a view initializer that passed data to the child. Because ThirdView is now created in ContentView, it seems like I would need to know that data upfront rather than be able to fetch it in SecondView.Downstairs
You can add navigationDestination in SecondView as well, which ever is nearest it will go for that. So in your case SecondView.Heidi
It looks like I'll have to use unique types for each level. Upon running, I get stuck in a loop at SecondView, and Swift warns: A navigationDestination for “EnvironmentStack.AppView” was declared earlier on the stack. Only the destination declared closest to the root view of the stack will be used.Downstairs
So you mean it is pushing twice?Heidi
You can also use the associated type with an enum or simply go with an array of strings as the path. Then you can handle your each navigation from ContentView it selfHeidi

© 2022 - 2024 — McMap. All rights reserved.