Recommended Reading

14 Minutes
SwiftUI Navigation Explained: Best Practices for Seamless App Flow
Fix Bugs Faster! Log Collection Made Easy
Navigation is one of the most basic functionalities of any app, and among the most crucial aspects of our work as developers.
From replacing a login screen with our actual logged-in state app, to showing a modal with details of any item inside our app, all of these are navigational challenges we need to tackle in our day-to-day.
SwiftUI has introduced a modern approach to navigation in Apple-based platforms. It replaces the older UINavigationController navigational system, which had issues around reliability and consistency, with different, more modern approaches. These new approaches can take a bit of getting used to, so we’re going to unpack them today.
This article will be very hands-on and give you examples on how to navigate in scenarios that you will find in your own apps. All the insights are drawn from our own work as devs, building navigation into all kinds of projects.
But that’s enough intro. Let’s get into it.
Table of Contents
We typically use the NavigationStack
and NavigationLink
components to handle navigation In our apps.
Example
import SwiftUI
struct FirstView: View {
var body: some View {
NavigationStack {
VStack {
Text("Navigation article")
.font(.title)
.padding()
NavigationLink("Go to the second view", destination: SecondView())
.padding()
}
.navigationTitle("First View")
}
}
}
struct SecondView: View {
var body: some View {
Text("This is the Second view")
.font(.headline)
.padding()
.navigationTitle("Second View")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
FirstView()
}
}
Now let’s take a detailed look at the most important navigational aspects:
- NavigationStack: This stack will handle our navigation. We wrap our content with it.
- NavigationLink: This creates the link that will allow us to navigate to a different page.
- .navigationTitle: This allows us to define a title to each of our pages on the Navigation bar.
2. Injecting data to views
We’re going to go slightly off-track here, if that’s ok. Injecting data into views isn’t directly related to navigation, but we often need to pass data between views, and this may still be useful when sharing new views.
While there are many ways for views to access the business logic, and data, they need, we will look into a simple way of injecting data from one view to another.
Example
import SwiftUI
struct FirstView: View {
var recipes: [String]
init(recipes: [String]) {
self.recipes = recipes
}
var body: some View {
NavigationStack {
List(0..<recipes.count) { index in
NavigationLink("\\(recipes[index])", destination: DetailView(recipe: recipes[index]))
}
.navigationTitle("Recipe List")
}
}
}
struct DetailView: View {
let recipe: String
var body: some View {
Text("Here are the recipe details")
.font(.largeTitle)
.padding()
.navigationTitle(recipe)
}
}
And to launch our app:
@main
struct OurApp: App {
var body: some Scene {
WindowGroup {
FirstView(recipes: ["Garlic bread", "Pepperoni Pizza", "Beef Gnocchi", "Fish and Chips"])
}
}
}
Now, looking at both examples together:
- The
DetailView
accepts a parameterrecipe
, which is provided when creating theNavigationLink
. The same exact thing happens with ourFirstView
: it accepts an array of recipes that we need to feed it. - Each
NavigationLink
dynamically passes its recipe name to theDetailView
and we then use those as the titles of theDetailView
.
The navigation we’ve seen so far is explicitly based on tapping a certain view. However, sometimes we might want to navigate forward/backward depending on the @State
of our View
. So let’s look at how to do this:
Example
import SwiftUI
struct FirstView: View {
@State private var isDetailViewActive = false
var body: some View {
NavigationStack {
VStack {
Button("Go to Details") {
isDetailViewActive = true
}
.padding()
.navigationDestination(isPresented: $isDetailViewActive, destination:{ DetailView()})
}
.navigationTitle("First View")
}
}
}
struct DetailView: View {
var body: some View {
Text("This is the detail view")
.font(.headline)
.padding()
.navigationTitle("Detail View")
}
}
- The
isDetailViewActive
state variable controls the activation of theNavigationLink
. - This is useful when there’s the need to control when/where we should show a different page.
Important note:
While this is a very useful resource to navigate, be careful on how to use it. The user may find it odd if an app just navigates without telling them why, so don’t use this feature without contemplating user experience.
Lists play a key role in many of our apps, and the cool thing is that integrating navigation within them is simple. We’ve already seen this on the second example, when we injected data into the next shown View, but we didn’t focus on that. So now let’s look again:
Example
import SwiftUI
struct FirstView: View {
var recipes: [String]
init(recipes: [String]) {
self.recipes = recipes
}
var body: some View {
NavigationStack {
List(0..<recipes.count) { index in
NavigationLink("\\(recipes[index])", destination: DetailView(recipe: recipes[index]))
}
.navigationTitle("Recipe List")
}
}
}
struct DetailView: View {
let recipe: String
var body: some View {
Text("Here are the recipe details")
.font(.largeTitle)
.padding()
.navigationTitle(recipe)
}
}
Two importan things are happening here:
List
generates rows dynamically for each item in therecipes
array.- Each row contains a
NavigationLink
that leads to theDetailView
for the selected item, in which we then know what item was previously selected.
5. Customizing Appearance
One of the great things about SwiftUI is that you can customize the navigation bar’s appearance, including adding buttons or changing UI styles.
Let’s look at an example of add a few of those buttons and, additionally, how we can navigate from them, modally, to new Views
.
Example
import SwiftUI
struct FirstView: View {
@State var areSettingsSelected: Bool = false
@State var isHelpSelected: Bool = false
var body: some View {
NavigationStack {
VStack {
Text("Main View")
.font(.largeTitle)
.navigationDestination(isPresented: $areSettingsSelected, destination:{ SettingsView()})
.navigationDestination(isPresented: $isHelpSelected, destination:{ HelpView()})
}
.navigationTitle("Main page")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Settings") {
areSettingsSelected = true
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Help") {
isHelpSelected = true
}
}
}
}
}
}
struct SettingsView: View {
var body: some View {
Text("This is the Settings view")
.font(.headline)
.padding()
.navigationTitle("Settings")
}
}
struct HelpView: View {
var body: some View {
Text("This is the Help view")
.font(.headline)
.padding()
.navigationTitle("Help")
}
}
- Use the
.toolbar
modifier to add custom buttons or other controls to the navigation bar. - The
ToolbarItem
placement determines where the control appears. - We use the technique shown before, on 3, to show the user
Settings
orHelp
depending on theView State
.
For any less basic navigational needs, SwiftUI provide us with the NavigationPath
APIs, that help us handle the navigational stack.
Example
import SwiftUI
struct FirstView: View {
@State private var path = NavigationPath()
let fruits: [Fruit] = [Fruit(name: "Apple"), Fruit(name: "Banana"), Fruit(name: "Strawberry")]
var body: some View {
NavigationStack(path: $path) {
List(fruits) { fruit in
Button(fruit.name) {
path.append(fruit.id)
}
}
.navigationDestination(for: String.self) { fruit in
DetailView(fruit: fruit)
}
.navigationTitle("Fruits")
}
}
}
struct DetailView: View {
let fruit: String
var body: some View {
Text("You selected \\(fruit)")
.font(.title)
.padding()
.navigationTitle(fruit)
}
}
struct Fruit: Identifiable {
var id: String
var name: String
init(name: String) {
self.name = name;
self.id = name
}
}
Things of note:
NavigationPath
allows us to set the fruit’s ID as a new path node. This can later be used to dismiss all Views until we reach a destination one.- Use
.navigationDestination(for:)
to specify how to handle destinations dynamically, based on the type of the path element. So we could have different things, like animals, and it would handle them accordingly for each type.
Conclusion
SwiftUI’s navigation tools, including NavigationStack
, NavigationLink
, and NavigationPath
, provide the flexibility to build intuitive, dynamic navigation flows in your apps. From simple links to complex navigation paths, SwiftUI ensures your app’s navigation is smooth and user-friendly.
Explore the examples above, adapt them to your projects, and you’ll be able to maximize SwiftUI’s capabilities to create great user experiences.
Happy coding!
Expect The Unexpected!
Debug Faster With Bugfender