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.