Routing & Navigation in SwiftUI — the right way
Learn how to create a light & simple routing layer to handle your application’s navigation
We would all agree that navigation is one of the most important pieces of our applications. Having a good routing system could be a huge difference when the time to scale our app and add new features come.
Navigation was the Achilles heel from day one in SwiftUI. The routing system was built highly coupled to the views, which makes things harder if we are building complex applications with a bunch of different flows.
Fortunately, from iOS 16 we have a new navigation API that allows us to maintain a stack of views like we used to do in UIKit, to push and pop views in a much easier way.
New Navigation APIs
Let’s create a simple application to show the different rooms that our house has. The first thing we need is a model to store the information about the room that we want to display. In this case, we will be showing the room’s name and a related image of that room.
In addition, we need a view to show the room’s information.
The last part that our application needs is a view of a list of all the rooms that we want to show. If the user taps on any of the items on the list, then we will navigate to the RoomDetail
view.
Let’s explore the different options that we have to accomplish this.
Navigation Link
The simplest way to route the user into a new view is using NavigationLink. As we will be using an array of RoomInfo
, we can use the init(_:destination:) method together with navigationDestination(for:destination:) modifier, where the destination will be every RoomInfo
instance that we have in the array.
What the navigationDestination modifier does, is link every item that we show in the list, with its respective data value. So when the user taps on any of the list items, it will capture the instance of RoomInfo for that list item.
Navigation Path
At this point, we know how to navigate in a simple master-detailed scenario when the view to which we want to navigate is the same (in our example, RoomDetail
). However, if we want to handle different types to push into the navigation stack, we'll need to use a new wrapper: NavigationPath. What NavigationPath does, is allow us to push any Hashable type into the stack.
Suppose that we want to add in our main view a new button. If the user taps that button, we will display a new list view with all the art paintings that we have. We can easily achieve this using NavigationPath.
Note that we can add as many navigationDestination modifiers as we need depending on the different types that our NavigationPath handles.
Routing
Now that we already saw how the new API works, we can start thinking about how we can create a custom Routing layer for our application.
First things first, we need to create a brand new class where we are going to store our NavigationPath.
This is all we need for our routing class. Inside the Destination enum, we can define all the possible navigation destinations. In our case, we will be changing our sample app and adding two buttons to navigate either to the living room view or to the bedroom view.
So, with this in place, every time that we want to navigate to any of the defined destinations, we just need to call navigate(to)
function. If we want to go to the previous view, just call navigateBack()
function. And, if we want to go to the root view, call navigateToRoot()
function.
Then, we need to create a Route instance from our application’s entry point and inject it as an environment object to be used in every view.
As you can see, we use the navigationDestination modifier, as we used it before, to create the correct view depending on the destination. Finally, we need to get the router instance from the views and use it whenever we need it.