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)

Nirav D
  • 71,513
  • 12
  • 161
  • 183
Collin Allen
  • 4,449
  • 3
  • 37
  • 52

1 Answers1

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.

Nirav D
  • 71,513
  • 12
  • 161
  • 183
  • 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. – Collin Allen Sep 15 '22 at 16:15
  • 2
    @CollinAllen It will if you go with `NavigationLink(value:)` – Nirav D Sep 15 '22 at 16:58
  • 2
    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. – Collin Allen Sep 18 '22 at 04:52
  • 2
    You can add `navigationDestination` in `SecondView` as well, which ever is nearest it will go for that. So in your case `SecondView`. – Nirav D Sep 18 '22 at 07:41
  • 1
    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. – Collin Allen Sep 19 '22 at 22:19
  • So you mean it is pushing twice? – Nirav D Sep 20 '22 at 03:49
  • 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 self – Nirav D Sep 20 '22 at 03:51