This was a headscratcher so I'm pasting the code sample below where I got it to work.
Suppose I have a view that will sit somewhere in my navigation stack, called SourceView
. From the perspective of the wider application, this view has some buttons or actions that should trigger navigation to one or more other views, which we'll call TargetView1
and TargetView2
.
The basic SwiftUI way of achieving this is to make SourceView
aware of this navigation requirement, and implementing the navigation inside SourceView
, by embedding a NavigationLink
in its view hierarchy. A NavigationLink
is a visible UI component akin to a button that a user can tap.
Following the standard approach, SourceView
needs to be aware of its role in a navigation stack, and of the View structs that it should navigate to when its buttons are tapped.
I would like to decouple it and make it possible to developer SourceView
in isolation, and in such a way that the behaviour associated with its buttons can be injected. In short, I want it to look like this:
struct SourceView: View {
let onButton1: () -> Void
let onButton2: () -> Void
var body: some View {
VStack {
Button("Button 1", action: onButton1)
Button("Button 2", action: onButton2)
}
}
}
and then inject the navigation behaviour when instantiating SourceView
in a root view whose only responsiblity is to set up the navigation plumbing. Something like this:
struct ContentView: View {
var body: some View {
SourceView(onButton1: onButton1, onButton2: onButton2)
}
private func onButton1() {
// Navigate to TargetView1 instance
}
private func onButton2() {
// Navigate to TargetView2 instance
}
}
The question was how to achieve this within SwiftUI's paradigm, especially considering how central NavigationLink
seems to be in the navigation architecture.
The solution involves initializing NavigationStack
with a binding to a path, and creating an empty NavigationLink
. Here's the code:
struct ContentView: View {
// NOTE 1
private enum NavigationItem {
case target1, target2
}
@State private var navigationPath: [NavigationItem] = []
var body: some View {
// NOTE 2
NavigationStack(path: $navigationPath) {
SourceView(
// NOTE 3
onButton1: { navigationPath.append(.target1) },
onButton2: { navigationPath.append(.target2) }
)
// NOTE 4
.navigationDestination(for: NavigationItem.self) { item in
switch item {
case .target1: TargetView1()
case .target2: TargetView2()
}
}
}
}
}
Notes from the code:
- Define some hashable identifier to represent possible navigation destinations, and create a state variable that is an array of that type to represent the navigation path
- Initialize
NavigationStack
with a binding to the path state - Perform navigation to a destination by appending the corresponding identifier to the path
- Add a
navigationDestination
modifier to the root view; this modifier provides concrete view instances for each destination identifier