I've read the documentation on the topic and I believe I have a basic understanding of it.
In theory, it sound pretty good, and not too complex. If i have a model class ModelClass
and a view to display the details of the model, ModelDetailsView
, I can just append the model instance to the NavigationPath
and have a .navigationDestination(for: ModelClass.self)
modifier ready to receive the data and display the details view.
So the project setup in simplified terms would be like this:
The ContentView
:
struct ContentView: View {
@StateObject private var navigator = Navigator()
var body: some View {
WindowGroup {
NavigationStack(path: $navigator.path) {
MainMenu()
.navigationDestination(for: ModelClass.self) { model in
ModelDetailsView(model: model)
}
}
.environmentObject(navigator)
}
}
}
The Navigator
class:
class Navigator: ObservableObject {
@Published var path = NavigationPath()
func navigate(with model: any Hashable) {
path.append(model)
}
}
And we have the navigation in the MainMenu
:
struct MainMenu: View {
@EnvironmentObject private var navigator: Navigator
let model = ModelClass()
var body: some View {
Button("Go to details") {
navigator.navigate(with: model)
}
}
}
I have multiple problems with the above mentioned method.
First, at the navigation part (navigator.navigate(with: model)
), I'm not really specifying my intentions in the code. From that single line, it's not clear if I'm trying to navigate to a details view, or to any other view that uses this class instance as a property. And adding comments kind of ruin the cleanliness of the swift code (for me at least).
But whatever, if I had to, I would write comments to clarify that this is supposed to take me to a details view. But this next problem is far worse...
So what if I have multiple views that take this exact class as property? Let's say I have this ModelDetailsView
, but I also have a SecondaryDetailsView
. These two views take only one argument, and it's a ModelClass
for both. Now, how should I write the .navigationDestination
modifier, to distinguish between these two? I could do
.navigationDestination(for: ModelClass.self) { model in
if someCondition {
ModelDetailsView(model: model)
} else {
SecondaryDetailsView(model: model)
}
}
but than, this someCondition
has to be in the ContentView
, which is neither practical nor a good practice.
Some place (I couldn't find the source) suggested to use String
s as identifiers when navigating. So the Navigator would have a function
func show<V>(_ viewType: V.Type) where V: View {
path.append(String(describing: viewType.self))
}
And that could be used like
navigator.show(ModelDetailsView.self)
// or
navigator.show(SecondaryDetailsView.self)
The problem with this one is that you can't pass arguments to the views.
.navigationDestination(for: String.self) { id in
if id == String(describing: ModelDetailsView.self) {
ModelDetailsView(model: ) // how do I get the model for the view?
}
}
So I'm wondering if anyone has a good solution to this, that could be used in even larger projects without getting too ugly.
As Benzy Neez suggested in the comments, it would be suitable to use an enum with associated values as the type of the navigation path. The names of each case can represent the type of view (whether it be
details
, orsecondaryDetails
), and the associated value can store the models you want to pass to the view.The
navigate
method can then take aDestination
:A usage like
navigate(to: .details(of: someModel))
would be very readable.The
navigationDestination(for:destination:)
modifier can check which case of the enum it is: