Reputation: 7459
I'm trying to migrate a coordinator pattern using UINavigationController
into the new NavigationStack
.
The navigation flow is quite complex, but I've made a simple project to simplify it to this:
NavigationStack
-> MainScreen
-> TabView
-> FirstTab
-> NavigationStack
-> FirstTabFirst
-> FirstTabSecond
-> SecondTab
-> SecondTabScreen
-> Second Top Screen
Althoug the first Screens in each NavigatorStack
are actually the root view of the navigator, it shouldn't be a problem as I can't even get it to navigate to something.
All the navigators have their state defined by a @StateObject
to allow for both programatic imperative navigation and NavigationLink
, similar to what a Coordinator pattern provides.
Upon launching the app, it immediatelly throws in the @main
line, with no further information about the call stack:
Thread 1: Fatal error: 'try!' expression unexpectedly raised an error: SwiftUI.AnyNavigationPath.Error.comparisonTypeMismatch
This is the code for the whole app:
import SwiftUI
protocol NavigationRoute: Hashable {
associatedtype V: View
@ViewBuilder
func view() -> V
}
enum Top: NavigationRoute {
case first, second
@ViewBuilder
func view() -> some View {
switch self {
case .first: TopFirst()
case .second: TopSecond()
}
}
}
enum Tab: NavigationRoute {
case first, second
@ViewBuilder
func view() -> some View {
switch self {
case .first: TabFirst()
case .second: TabSecond()
}
}
var label: String {
switch self {
case .first: return "First"
case .second: return "Second"
}
}
var systemImage: String {
switch self {
case .first: return "house"
case .second: return "person.fill"
}
}
@ViewBuilder
func tab() -> some View {
view()
.tabItem { Label(label, systemImage: systemImage) }
.tag(self)
}
}
enum FirstTab: NavigationRoute {
case first, second
@ViewBuilder
func view() -> some View {
switch self {
case .first: FirstTabFirst()
case .second: FirstTabSecond()
}
}
}
class StackNavigator<Route: NavigationRoute>: ObservableObject {
@Published var routes: [Route] = []
}
class TabNavigator<Route: NavigationRoute>: ObservableObject {
@Published public var tab: Route
public init(initial: Route) {
tab = initial
}
public func navigate(_ route: Route) {
tab = route
}
}
@main
struct NavigationStackTestApp: App {
@StateObject var navigator = StackNavigator<Top>()
var body: some Scene {
WindowGroup {
NavigationStack(path: $navigator.routes) {
TopFirst().navigationDestination(for: Top.self) {
$0.view()
}
}
}
}
}
struct TopFirst: View {
@StateObject var navigator = TabNavigator<Tab>(initial: .first)
var body: some View {
TabView(selection: $navigator.tab) {
Tab.first.tab()
Tab.second.tab()
}
}
}
struct TopSecond: View {
var body: some View {
Text("Top Second")
}
}
struct TabFirst: View {
@StateObject var navigator = StackNavigator<FirstTab>()
var body: some View {
NavigationStack(path: $navigator.routes) {
FirstTabFirst().navigationDestination(for: FirstTab.self) {
$0.view()
}
}
}
}
struct TabSecond: View {
var body: some View {
Text("Tab Second")
}
}
struct FirstTabFirst: View {
var body: some View {
Text("First Tab First")
}
}
struct FirstTabSecond: View {
var body: some View {
Text("First Tab Second")
}
}
To avoid it from crashing I have to replace the NavigationStack
of the first tab with an EmptyView()
, that is changing:
case .first: TabFirst()
to
case .first: EmptyView()
But of course after doing that the first tab is missing and the app doesn't do what it's supposed to.
Has anyone encountered something like this or has an idea of how to get this working? SwiftUI is closed source and it's documentation is very limited so it's really hard to know what's really going on under the hood.
EDIT:
Using NavigationPath
as state for StackNavigator
doesn't crash the app, but any navigation in the first tab's NavigationStack
pushes a new view in the top navigator and hides the bottom tab bar.
I'm assuming you just can't have two navigation stacks in the same hierarchy and expect it to work as you would assume.
Upvotes: 1
Views: 953
Reputation: 7459
Using two NavigationStack
s in the same hierarchy doesn't work very well as their internal states collide, I would not recommend it and it seems to cause the error. The error is thrown when the lower navigator is declared because it's declared type is also handled by the top navigator, which doesn't have a handler declared for the lower navigator's tap in its navigationDestination
.
It is better to use conditional rendering and manual animation/transitions in the upper level and then from there render only one NavigationStack
per hierarchy. There is no issue using NavigationStack
inside TabView
as long as that TabView
is not itself inside a NavigationStack
.
NavigationPath
can be used to support multiple unrelated types in the state stack.
Upvotes: 4