Dealing with Nested NavigationStacks in SwiftUI?
Asked Answered
S

1

9

I've looked around but haven't seen any info on this. If I want to have multiple navigation stacks in SwiftUI, and have multiple navigation links that add to different navigation stacks, how can I do this? In my use case I use a tab view, so I would be something like:

NavigationStack(path: $globalNavigationStack) {
  TabView {
    PrimaryView()
    SecondaryView()
  }
}

Then maybe in PrimaryView:

struct PrimaryView: View {
  var body: some View {
    NavigationStack(path: $localNavigationStack) {
       NavigationLink(value: SomeType.self) {
         Button("This should add to local stack")
       }
       .navigationDestination(for: SomeType.val) { someType in
          ViewOnLocalStack() 
       }

       NavigationLink(value: SomeType.self) {
         Button("This should add to global stack")
       }
       .navigationDestination(for: SomeType.val) { someType in
          ViewOnGlobalStack() 
       }
    }
  }
}

But the second NavigationLink will add to the local stack. Assuming I have access to the global path, how can I make the second one add to global path? What I tried was this:

NavigationStack(path: $globalNavigationStack) {
  TabView {
    PrimaryView()
    SecondaryView()
  }
}
.navigationDestination(for: SomeType.val) { someType in
    ViewOnGlobalStack() 
}

along with removing the onDestination from the second link, but the console complained that the navigation link had no corresponding onDestination. Not sure if I've seen anyone else have a similar situation.

Squinty answered 21/5, 2023 at 19:32 Comment(4)
Each tab can have a NavigationStack but a TabView shouldn't be inside one. They should be "mutually exclusive" per the human interface guidelines. that global stack goes against Apple's designSnailfish
Did you come up with a solution? Nested Navigation by UINavigaitonViewController perfectly works on UIKit. But I have an issue with "pop" and "pop to root" in SwiftUI.Nozzle
Anyone find luck?Larceny
I've posted my solution in case it helps. TLDR nested stacks aren't possible.Luger
L
1

TLDR; Nested NavigationStack isn't possible. You'll need a mixed approach. The following answer is advice on how to approach it assuming nesting is not possible.

XCode will not necessarily complain if your try. But even if you're able to make it work your app will quickly crash.

My advice: don't try to hack it. NavigationStack is decent at managing navigation state at one level deep. To create nuance in your view states, you will either have to create numerous views with isolated state or fewer robust views with shared state.

Here is the bad approach versus some suggested approaches in tree structure. Note that I am using the term "screen" because view has a specific meaning in SwiftUI.

How you're currently imagining your app navigation scheme:

Bad approach

App <- NavigationStack
│
├── Screen1 <- NavigationStack
│   ├── Subscreen1A
│   └── Subscreen1B
│
└── Screen2 <- NavigationStack
    ├── Subscreen2A
    └── Subscreen2B

Better approach: your root level contains NavigationStack and a custom global state object.

App <- NavigationStack and global state object for subviews.
│
├── Screen1 <- reacts to global state object for changes
├── Screen2 <- reacts to global state object for changes

More complex approach: screens don't share state.

App <- NavigationStack
│
├── Screen1 <- custom state object with conditionally rendered subviews
├── Screen2 <- custom state object with conditionally rendered subviews

So I'll note a few points of guidance for how to approach subviews:

  • Create state objects for complex views: If state must persist beyond destruction of a view, then put it in a parent view or global state.
  • Use booleans or enums to conditionally render subviews: Beyond the initial NavigationStack, use these data types to determine how to render the entire view or parts of your view.
  • Avoid the "view model" abstraction: Because of how SwiftUI manages view lifecycles, it simply won't work.
  • Avoid UIKit if possible: If you find yourself in a situation where you need to include UIKit and SwiftUI in an app, be prepared for a ton of glue code. Even if you're not using UIKit directly, UIKit-inspired advice can be misleading.
  • Abstract early and often: Brevity is not a quality I'd ascribe to SwiftUI (sure it's better than UIKit but pain is pain). Encapsulate obvious responsibilities (like stateless logic or data structures).

Lastly, here's an example to illustrate the "better approach" I mentioned earlier. It'll get you started assuming a global state works for now.

import SwiftUI

enum AppRoute: Hashable {
  case screenOne
  case screenTwo
  case screenThree
}

final class AppState: ObservableObject {
  @Published var path = NavigationPath()
  // Can't be optional.
  // Manage in parent to inject state into "child" views
  @Published var screenTwoData = ?
  @Published var isScreenTwoFlowActive: Bool = false
}

@main

struct MyApp: App {
  @StateObject private var appState = AppState()

  var body: some Scene {
    WindowGroup {
      NavigationStack(path: $appState.path) {
        VStack {
          ScreenOne()
        }
        .navigationDestination(for: AppRoute.self) { appRoute in
          switch appRoute {
          case .screenOne:
            ScreenTwo()
          case .screenTwo:
            ScreenThree()
          }
        }
      }
      .environmentObject(appState)
    }
  }
}
Luger answered 8/5 at 10:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.