Skip to content

Recommended Reading

SwiftUI Navigation Explained: Best Practices for Seamless App Flow

14 Minutes

SwiftUI Navigation Explained: Best Practices for Seamless App Flow

Fix Bugs Faster! Log Collection Made Easy

Get started

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.


1. Navigation Basics

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 parameter recipe, which is provided when creating the NavigationLink. The same exact thing happens with our FirstView: it accepts an array of recipes that we need to feed it.
  • Each NavigationLink dynamically passes its recipe name to the DetailView and we then use those as the titles of the DetailView.

3. Navigation depending on our State

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 the NavigationLink.
  • 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.


4. Navigation in Lists

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 the recipes array.
  • Each row contains a NavigationLink that leads to the DetailView 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 or Help depending on the View State.

6. Deep Linking and Navigation Paths

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

Start for Free
blog author

Flávio Silvério

Interested in how things work since I was little, I love to try and figure out simple ways to solve complex issues. On my free time you'll be able to find me with the family strolling around a park, or trying to learn something new. You can contact him on Linkedin or GitHub

Join thousands of developers
and start fixing bugs faster than ever.