Search code examples
swiftuiswiftui-navigationstack

NavigationStack not affected by EnvironmentObject changes


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)


Solution

  • 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.