After realizing the lookup and editing limitations of the built-in, type-erased NavigationPath, I'm exploring an enum-based solution. With this approach, I can inspect and edit the path much easier while still combining multiple types.
Besides sharing this solution, I want to know if there are any drawbacks with this approach. Do I lose any functionality that the native NavigationPath offered?
import SwiftUI
struct WithRouteEnumNavigationPath: View {
@State var navigationPath = [Route]()
var body: some View {
NavigationStack(path: $navigationPath) {
Text("Root")
.navigationDestination(for: Route.self) { route in
switch route {
case .student(let student):
StudentView(student: student)
case .teacher(let teacher):
TeacherView(teacher: teacher)
}
}
}
.overlay {
VStack {
Text("Add Student")
.onTapGesture {
navigationPath.append(.student(Student()))
}
Spacer()
Text("Add Teacher")
.onTapGesture {
navigationPath.append(.teacher(Teacher()))
}
}
}
}
}
struct StudentView: View {
let student: Student
var body: some View {
Text("Student id \(student.id)")
}
}
struct TeacherView: View {
let teacher: Teacher
var body: some View {
Text("Teacher id \(teacher.id)")
}
}
struct Student: Hashable {
let id = UUID()
}
struct Teacher: Hashable {
let id = UUID()
}
enum Route: Hashable {
case student(_ student: Student)
case teacher(_ teacher: Teacher)
}
#Preview {
WithRouteEnumNavigationPath()
}
This second code snippet shows the same scenario with the built-in NavigationPath:
import SwiftUI
struct WithTypeErasedNavigationPath: View {
@State var navigationPath = NavigationPath()
var body: some View {
NavigationStack(path: $navigationPath) {
Text("Root")
.navigationDestination(for: Teacher.self) { teacher in
TeacherView(teacher: teacher)
}
.navigationDestination(for: Student.self) { student in
StudentView(student: student)
}
}
.overlay {
VStack {
Text("Add Student")
.onTapGesture {
navigationPath.append(Student())
}
Spacer()
Text("Add Teacher")
.onTapGesture {
navigationPath.append(Teacher())
}
}
}
}
}
struct StudentView: View {
let student: Student
var body: some View {
Text("Student id \(student.id)")
}
}
struct TeacherView: View {
let teacher: Teacher
var body: some View {
Text("Teacher id \(teacher.id)")
}
}
struct Student: Hashable {
let id = UUID()
}
struct Teacher: Hashable {
let id = UUID()
}
#Preview {
WithTypeErasedNavigationPath()
}
Do I lose any functionality that the native NavigationPath offered?
Yes. By using a homogeneous navigation path, you effectively made your navigation very centralised.
Suppose when building your app, you found that in addition to teachers and students, you also need to navigate to views representing School
s. If you use a NavigationPath
, you can just add a navigationDestination
to whichever view that needs to show schools. For example, there might be a SchoolsList
view, whose body
looks like this:
List(schools) { school in
NavigationLink(school.name, value: school)
}
.navigationDestination(for: School.self) {
SchoolView($0)
}
You only need to change the view that is concerned about School
s (SchoolsList
in this case), and the other views don't need to change at all.
If you use a Route
enum instead, you need to add an extra case to the enum, and add a extra switch
case in the navigationDestination
that is applied to the root of the navigation stack. You effectively lose separation of concern - the root of the navigation stack needs to know about every kind of view that will potentially be navigated to.
enum Route: Hashable {
case student(Student)
case teacher(Teacher)
case school(School) // <-----
}
// ...
NavigationStack(path: $navigationPath) {
Text("Root")
.navigationDestination(for: Route.self) { route in
switch route {
case .student(let student):
StudentView(student: student)
case .teacher(let teacher):
TeacherView(teacher: teacher)
case .school(let school): // <-----
SchoolView(school)
}
}
}
// SchoolsList will just be a List
List(schools) { school in
NavigationLink(school.name, value: Route.school(school))
}
Consequently this increases coupling of your views. You cannot easily reuse SchoolsList
anymore. Because the list rows in SchoolsList
are navigation links of type Route
, it must be used in a NavigationStack
that handles all Route
s, instead of just School
s.