Exploring Navigation in SwiftUI: A Deep Dive into NavigationView

Navigating through different screens is a cornerstone of virtually every mobile application. For those developing with SwiftUI, this task was typically handled by the NavigationView, a key component that managed and displayed a stack of views in a navigational context. However, as of iOS 16, NavigationView has been deprecated and replaced by two new views – NavigationStack and NavigationSplitView. In this blog post, we will explore the nuances of NavigationView for the benefit of those maintaining older apps, while also introducing you to these new navigation components that promise more stability and flexibility for your SwiftUI apps.

What is a NavigationView in SwiftUI?

NavigationView is on of the most important components in SwiftUI. It is used to manage and display a stack of views in a navigational context. It acts as a container for the hierarchical content, enabling the transition between different views through user interactions. In simpler terms, it’s the backbone of your app’s navigation system, providing a way for users to navigate through different sections or pages of your application.

What is the purpose of SwiftUI Navigation?

The primary role of NavigationView is to facilitate seamless navigation across different screens or views within your SwiftUI app. The interface it provides is intuitive and user-friendly, mirroring the navigation experience users have come to expect from modern mobile apps.

NavigationView in SwiftUI serves a dual purpose. On one hand, it’s a container for your views, but on the other, it also provides a context for features like the navigation bar, which can host title displays, navigation bar items, and even facilitate complex navigation actions.

One of the major benefits of using NavigationView is its adaptability. It adjusts its presentation based on the platform and device it’s running on. For example, on iOS devices, it presents content within a navigation interface, with support for hierarchical navigation, while on macOS, it creates a multi-column layout, providing a sidebar and a content area.

How to use NavigationView?

Creating a basic NavigationView in SwiftUI is a straightforward process. Below is a basic example:

struct ContentView: View {
    var body: some View {
        NavigationView {
            FirstColumnView()
            SecondColumnView()
            ThirdColumnView()
        }
    }
}

Each view in the navigation view represents a screen on iOS. For the iPhone, you will get a stack navigation style, where each column slides in from the trailing edge. On the iPad in fullscreen, you can see one or more columns. 

Because on the Mac there is much more space, the navigation view uses multiple columns to display all columns together.

swiftui example with NavigationView stack
SwiftUI example with NavigationView on the iPhone. You can navigate from the root view to the detail screen.
Swiftui navigation view on the iPad in portrait mode
SwiftUI navigation view on the iPad in portrait mode
Swiftui navigation view on the iPad in landscape mode
SwiftUI navigation view on the iPad in landscape mode
SwiftUI navigation view on the Mac
SwiftUI navigation view on the Mac. The sidebar can be collapsed to show 2 or 3 columns

Navigating Through the NavigationView

NavigationView in SwiftUI offers a way to navigate through a hierarchy of views, transitioning from one view to another based on user actions. When a new view is introduced into the hierarchy, it’s “pushed” onto the navigation stack, effectively making it the active view. The previous view remains in the stack, hidden beneath the new one. The user can “pop” the current view from the stack, reverting to the previous one, by using the back button in the navigation bar.

What are navigation actions like push, pop, etc.

The key navigation actions in SwiftUI include:

  1. Push: This action introduces a new view onto the navigation stack, making it the current view. It’s typically associated with user actions, like tapping a button or selecting a row in a list.
  2. Pop: This action removes the current view from the navigation stack, revealing the previous view. This action is usually initiated by the user tapping the back button in the navigation bar.
  3. Pop to Root: This action removes all views from the navigation stack except the root view. It’s useful for quickly navigating the user back to the starting point of your navigation hierarchy.

NavigationLink: How to move to a destination view

In the following you can see a simple navigation action. 

struct FirstColumnView: View {
    var body: some View {
        List {
            Text("First Column")

            NavigationLink("Open Next View") {
                SecondColumnView()
            }
        }
        .navigationTitle("First Navigation Title")
    }
}

I am using a navigation link, which is a special SwiftUI button. When you tap on the navigationlink destination text `Open Next View`, a new view is pushed on the navigation stack. You set the new view by defining the navigation destination in the navigation link. In the above example, I am moving to the new view `SecondColumnView`.

The first view inside a NavigationView is called RootView. Typically you would use a List view. When the user taps on one element in the list, you push to another view with more detailed information. Lists are very important for navigation. Learn more about List in this comprehensive tutorial.

Similarly, I am using another NavigationLink button to go from the second column to the third:

struct SecondColumnView: View {
    var body: some View {
        VStack {
            Text("Second Column View")
                .font(.title)

            NavigationLink("Open Next View") {
                ThirdColumnView()
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.yellow.opacity(0.3))
        .navigationTitle("Second Navigation Title")
    }
}
struct ThirdColumnView: View {
    var body: some View {
        Text("Third Column View")
            .font(.title)
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(Color.mint.opacity(0.5))
            .navigationTitle("Third Navigation Title")
    }
}

Programmatic Navigation: How to navigate programmatically in SwiftUI?

Remember that navigation in SwiftUI is data-driven, so you’ll often find yourself using state variables and bindings to control when and how navigation actions are performed. This approach is powerful and flexible, and it ties into SwiftUI’s overall reactive design. Use this if you want to programmatically push to a new view.

Learn how about SwiftUI programmatic navigation in the following Youtube tutorial:

How to customize the appearance of Navigation Bar

In SwiftUI, the navigation bar is a key component of the NavigationView. It’s typically displayed at the top of the screen and provides contextual information and controls related to the currently displayed view. The navigation bar can contain a title and a variety of navigation bar items, such as buttons, which can be used to trigger various actions.

You can customize the navigation bar’s appearance and content using various modifiers provided by SwiftUI. Here are some examples:

  • .navigationBarTitle(:) is used to set the navigation bar’s title. 
  • .navigationBarDisplayMode(:) can be set to .automatic (default), .inline, or .large. It is replaced by toolbarDisplayMode in iOS 17 and macOS 14
  • .navigationBarItems(leading:trailing:) is used to add items (navigation bar button) to the leading and trailing edges of the navigation bar. It is replaced by .toolbar() in iOS 14 and macOS 11.
  • .navigationBarBackButtonHidden() can be used to hide the default back button.
  • .navigationBarHidden(_:) can hide the entire navigation bar.

Here’s an example that demonstrates how to customize a navigation bar:

struct ContentView: View {
    var body: some View {
        NavigationView {
            Text("Hello, SwiftUI!")
                .navigationBarTitle("Welcome")
                .toolbar(content: {
                    ToolbarItem(placement: .topBarLeading) {
                        Button(action: {
                            print("Leading button tapped")
                        }) {
                            Image(systemName: "bell")
                                .foregroundColor(.blue)
                        }
                    }

                    ToolbarItem(placement: .primaryAction) {
                        Button(action: {
                            print("Trailing button tapped")
                        }) {
                            Image(systemName: "gear")
                                .foregroundColor(.blue)
                        }
                    }
                })
        }
    }
}
Free SwiftUI Layout Book

Get Hands-on with SwiftUI Layout.

Master SwiftUI layouts with real-world examples and step-by-step guide. Building Complex and Responsive Interfaces.

How to change the Navigation bar background color?

SwiftUI does not offer a build in solution to change the navigation bar background color. However, you can work with the underlying UIKit component which is UINavigationBar. In the following code, I am changing the background color of the entire stack to green:

struct ContentView: View {

    init() {
        let coloredNavAppearance = UINavigationBarAppearance()

        coloredNavAppearance.configureWithOpaqueBackground()
         coloredNavAppearance.backgroundColor = .systemGreen
         coloredNavAppearance.titleTextAttributes = [.foregroundColor: UIColor.white]
         coloredNavAppearance.largeTitleTextAttributes = [.foregroundColor: UIColor.white]

         UINavigationBar.appearance().standardAppearance = coloredNavAppearance
         UINavigationBar.appearance().scrollEdgeAppearance = coloredNavAppearance
    }

    var body: some View {
        NavigationView {
            FirstColumnView()
            SecondColumnView()
            ThirdColumnView()
        }
    }
}

This changes the UINavigationBar.appearance() for all navigation bars in you app. It is overriding the default behaviour. Therefor you only need to do this once in your app. 

SwiftUI navigation view example with custom navigation bar background color
SwiftUI navigation view example with custom navigation bar background color

Common Issues with NavigationView and How to Solve Them

While creating a NavigationView in SwiftUI is relatively simple, there are a few common pitfalls that you might encounter:

  1. Misplacement of modifiers: SwiftUI views and their modifiers are order-sensitive. For example, if you apply a modifier to the NavigationView instead of the Text view, it might not yield the expected result. Always ensure you’re applying your modifiers to the correct views.
  2. NavigationView Duplication: Avoid embedding a NavigationView within another NavigationView. This redundancy can cause unexpected behavior and should be avoided.
  3. Empty NavigationView: A NavigationView without any child views won’t display anything. Ensure you always have at least one view within your NavigationView.
  4. Overlooking platform differences: Remember that NavigationView can behave differently on different platforms (e.g., iOS vs. macOS). Always test your app on all target platforms to ensure consistent behavior.
  5. Programmatic Navigation can be very difficult to implement. I found it oftentimes is buggy when I do too much and overall unreliable. Its behavior can also vary depending on the iOS version and iPhone or iPad. 

NavigationView has a lot of problems and bugs. Most people never liked it and it might have been one of the main reasons why people think SwiftUI is not production ready. Luckily Apple addressed these problems and introduced the new NavigationStack and NavigationSplitView, which are much more stable and offer more flexibility.

Conclusion

Mastering navigation in SwiftUI is crucial to crafting intuitive and user-friendly apps. While NavigationView has served as a fundamental tool for SwiftUI developers, the transition to NavigationStack and NavigationSplitView in iOS 16 offers an exciting new frontier in app navigation. As we look forward to the enhanced stability and flexibility these new views provide, understanding the nuances of NavigationView remains essential for maintaining and updating older apps. Through continuous learning and adaptation, we can ensure our apps deliver the seamless navigation experience users have come to expect. Happy coding!

Further Reading:

Leave a Comment

Subscribe to My Newsletter

Want the latest iOS development trends and insights delivered to your inbox? Subscribe to our newsletter now!

Newsletter Form