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
NavigationStackwith a binding to the path state - Perform navigation to a destination by appending the corresponding identifier to the path
- Add a
navigationDestinationmodifier to the root view; this modifier provides concrete view instances for each destination identifier