The new NavigationStack & NavigationPath for SwiftUI
Tags: swift, swiftui, navigation, wwdc
Published: 2022-06-09 13:55:30


##The Navigation API
Apple recently released their improved method of navigation in SwiftUI, which was announced at WWDC 2022. 

Whilst the API is _very likely_ to change and evolve before the full release, this article looks at how it can be implemented to handle some trickier navigation tasks in native SwiftUI.

---

##Navigation in SwiftUI so far
Programmatic navigation and deep linking has been more challenging for newer developers, such as myself, to adopt in purely SwiftUI projects. 

I have mostly relied on `NavigationView` and `NavigationLink` to perform the navigation within my apps, which is functional but pretty basic and limited in nature.

With the introduction of `NavigationStack`, `NavigationPath`, `navigationDestination` modifier and adjustments to `NavigationLink`, more complex navigation tasks such as deep linking and popping multiple views/popping to root become easier to implement purely in SwiftUI. 

---

##Holiday Destinations - Example
In this sample project, I have a few screens that can be navigated between. 
1. `DestinationListView` - A view containing a list of the available holiday destinations
2. `DestinationDetailView` - A view that provides more detail about the destination that was selected from the above list view
3. `DestinationInfoView` / `DestinationBookingView` - Views that would allow the user to view more information or book the destination. These can be accessed from the detail view via corresponding buttons

---

###Project Setup
To begin, I created a basic data model, `DestinationModel`. This must conform to Hashable to be used with the [navigationDestination](https://developer.apple.com/documentation/swiftui/view/navigationdestination(for:destination:)) view modifier later. Alongside this model, I created a `mockDestinations` property that stores some sample data, which can be viewed in the full file below.

```swift
struct DestinationModel: Hashable, Identifiable {
    let id = UUID()
    let location: String
    let pricePerPerson: Double
    let description: String

    static let mockDestinations: [DestinationModel] = [
        ...
    ]
}
```
Full file: [DestinationModel.swift](https://gist.github.com/Kuncans/9058f9da75811b7988636d31158549e6)

_(note: if you have a more complex data model with properties that do not conform to Hashable, you will need to add a [hash(into:)](https://www.hackingwithswift.com/example-code/language/how-to-conform-to-the-hashable-protocol) method manually)_

---

###Storing Navigation State
In order to keep track of the navigation routes that our app has taken, it is best to abstract the storage of this information as a navigation path outside of the views.

I created an `ObserverableObject` class `Router` to do just this. 

```swift
final class Router: ObservableObject {
    
    @Published var navPath: NavigationPath = .init()
    
}
```

###Providing Access to the Navigation State in a View
Then, instantiate this in the root view of the app as a `StateObject`, which can then be injected to the child views as an `environmentObject`. This enables us to gain access to the properties of our `Router` object in any view.

```swift
@main
struct NavigationStackApp: App {
    
    @StateObject private var router = Router()
    
    var body: some Scene {
        WindowGroup {
            NavigationStack(path: $router.navPath) {
                DestinationListView()
                    .navigationBarTitleDisplayMode(.inline)
            }
            .environmentObject(router)
        }
    }
}
```

_(note: We are also wrapping our root view in the new `NavigationStack`, so that we can use the Navigation API in all child views using the `@EnvironmentObject` property wrapper)_

```swift
struct DestinationListView: View {
    
    @EnvironmentObject var router: Router
    
    var body: some View {
      ...
    }
}
```

---

###Passing a value to `NavigationLink`

Previously when using `NavigationLink`, we would typically pass a `destination` and `label` as parameters for a new view. 

Now, we can use a new completion that passes a `value` and `label`. The value must conform to Hashable, which our model already does. We can use the destination that is being iterated on in our list, to pass into the `NavigationLink` as a `value`. We can then use a closure to provide the `label` for the NavigationLink, just as before.

```swift
struct DestinationListView: View {
    
    @EnvironmentObject var router: Router
    
    var body: some View {
        List {
            ForEach(DestinationModel.mockDestinations) { destination in
                NavigationLink(value: destination) {
                    VStack (alignment: .leading, spacing: 10) {
                        Text(destination.location)
                            .font(.headline)
                        Text("\(destination.pricePerPerson, format: .currency(code: "USD"))")
                            .font(.subheadline)
                            .foregroundColor(.secondary)
                    }
                }
            }
        }
    }
}
```

###Setting the destination for `NavigationLink`
We can then add the `navigationDestination` modifier to the outside of our list, and provide the type of model that we have declared in our `NavigationLink(value: _)`. This allows our `NavigationPath` to know the type of element that is being appended to the stack, and since it is hashable, it can also be identified and compared by it's hashValue. 

For each destination we pass to the navigationDestination, we can then provide the view to be navigated to, as well as pass in any properties that view may require.

```swift
.navigationDestination(for: DestinationModel.self) { destination in
                DestinationDetailView(destination: destination)
            }
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p1vgm69t5gzxlsz96vp2.png)

Full file: [DestinationListView.swift](https://gist.github.com/Kuncans/8b1f08b852b46dfe456abdf61b1a8b7e)

---

###Destination Detail View

This is the view that will be shown when a destination in the previous list view was tapped.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oe3pjb6e66nas87if43y.png)

```swift
struct DestinationDetailView: View {
    
    @EnvironmentObject var router: Router
    
    let destination: DestinationModel
    
    var body: some View {
        VStack(alignment: .center, spacing: 50) {
            Text(destination.location)
                .font(.title)
            Text(destination.description)
                .font(.body)
            VStack {
                Text("Price per Person")
                Text("\(destination.pricePerPerson, format: .currency(code: "USD"))")
                    .bold()
            }
        }
        .padding(.horizontal)
    }
}

struct DestinationDetailView_Previews: PreviewProvider {
    static var previews: some View {
        DestinationDetailView(destination: DestinationModel.mockDestinations.first!)
            .environmentObject(Router())
    }
}

```
The two buttons both toggle a sheet to show a different view (`DestinationBookingView` or `DestinationInfoView`). We can add a property to our router to allow us to programmatically toggle the sheet. 

```swift
@Published var showSheet = false
```

We can also start leveraging the power of Swift to allow us to switch our navigation path's route. By declaring an enum, and using `@ViewBuilder`, we can be more dynamic in which sheet is shown.

```swift
enum Router {
        case info
        case booking
        
        @ViewBuilder
        var view: some View {
            switch self {
            case .info:
                DestinationInfoView()
            case .booking:
                DestinationBookingView()
        }
    }
}
    
var nextRoute: Router = .booking
```

By adding the property `nextRoute`, we are able to change which view will be displayed on the sheet on the fly.

Full file: [Router.swift](https://gist.github.com/Kuncans/902db67af6117e6e7919685a4ac9f88f)

In our `DestinationDetailView` file, we can now alter the properties on the router dependent on which button has been pressed. 

```swift
Button("Info") {
    router.nextRoute = .info
    router.showSheet.toggle()
}
                
Button("Book") {
    router.nextRoute = .booking
    router.showSheet.toggle()
}
```

Remember to also create a sheet using the `sheet` modifier. Bind this to the Router's `showSheet` property, and the view that will be displayed will be the returned value of the `@ViewBuilder` we used in our Router class.

```
.sheet(isPresented: $router.showSheet) {
    router.nextRoute.view
}
```

Full file: [DestinationDetailView.swift](https://gist.github.com/Kuncans/5468e6b8cf2edbe938ac7cfb39679682)

---

###Popping to the root view
So far, we haven't navigated too deep into our app, but the following will demonstrate how you can pop to the root view of your `NavigationPath`.

_(note: the `DestinationBookingView` and `DestinationInfoView` contain almost identical code, as they are placeholder views)_

By reinitialising our Router's `navPath` property, we are essentially saying that the `NavigationPath` is empty, and that are stack should contain no additional views. 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xfkb6d4dkr9iqyju9r5c.png)

```swift
Button("Pop to root") {
    router.showSheet = false
                
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        router.navPath = .init()
    }
}
```

This will dismiss the sheet, and after a short delay, navigate back to the root view, and clears any additional data in the `router.navPath` property.

[DestinationBookingView.swift](https://gist.github.com/Kuncans/16f0a02c98783c06d899e57ebb6e3f82)
[DestinationInfoView.swift](https://gist.github.com/Kuncans/9a0c26ef87fe09facac12497dfc241f8)

---

###Deeplinking and further implementations
With the setup used above, we are able to dynamically switch the path of the navigation router, as well as programmatically pass particular data models when navigating.

The below is an example of navigating to a random element in the array of mock destinations, and then opening a specific sheet. We can also detmine which sheet by adjusting the `nextRoute` property of Router.

In `DestinationListView`:
```swift
.onAppear {
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
       router.navPath.append(DestinationModel.mockDestinations.randomElement()!)
       router.nextRoute = .info
    }
}
```
In `DestinationDetailView`:
```swift
.onAppear {
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
        router.showSheet.toggle()
    }
}
```

You can also programmatically navigate using the array's index of the element you wish to pass. For example, you could navigate to the first destination that contains the following for the `location` property: "Costa Rica, Central America".

In `DestinationDetailView`:

```swift
.onAppear {
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
        guard let index = DestinationModel.mockDestinations.firstIndex(where: { $0.location == "Costa Rica, Central America" }) else { return }
            router.navPath.append(DestinationModel.mockDestinations[index])
            router.nextRoute = .info
    }
}
```

---

##Finishing Thoughts
You could continue further, and build up a variety of different routes your app could take by creating a `Route` object, and then passing an array of this to your router to handle. However, I'm not quite there with the implementation just yet myself!

Any feedback/improvements/mistakes to correct is always appreciated.

The full repo can be found [here](https://github.com/Kuncans/NavigationStack/).