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)
}
}
}
tab
can have aNavigationStack
but aTabView
shouldn't be inside one. They should be "mutually exclusive" per the human interface guidelines. that global stack goes against Apple's design – Snailfish