Skip to content

Instantly share code, notes, and snippets.

@phlippieb
Last active May 31, 2024 12:11
Show Gist options
  • Save phlippieb/b7c7fbcb2078d896c56b215c31ec5293 to your computer and use it in GitHub Desktop.
Save phlippieb/b7c7fbcb2078d896c56b215c31ec5293 to your computer and use it in GitHub Desktop.

Injecting navigation behaviour into SwiftUI Views

This was a headscratcher so I'm pasting the code sample below where I got it to work.

Context

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.

Problem

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.

Solution

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:

  1. 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
  2. Initialize NavigationStack with a binding to the path state
  3. Perform navigation to a destination by appending the corresponding identifier to the path
  4. Add a navigationDestination modifier to the root view; this modifier provides concrete view instances for each destination identifier
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment