SwiftUI Navigation with NavigationView and NavigationStack

SwiftUI navigation is how users move from one screen to another in your app. In practice, this means placing content inside a navigation container, creating links to destination views, setting titles and toolbars, and sometimes controlling the navigation path in code. Understanding both NavigationView and NavigationStack matters because many older SwiftUI examples still use NavigationView, while modern apps should usually use NavigationStack.

Quick answer: Use NavigationStack for modern SwiftUI apps, especially on iOS 16 and later. NavigationView is the older API, still seen in existing code, but NavigationStack gives clearer and more flexible navigation, including path-based programmatic navigation.

Difficulty: Beginner

Helpful to know first: You will understand this better if you know basic Swift syntax, how SwiftUI views are built with body, and simple state handling with @State.

1. What Is SwiftUI Navigation?

SwiftUI navigation is the system that lets a user move deeper into app content and return again. A common example is tapping a row in a list to open a detail screen.

Think of navigation as a stack of screens. The root screen is at the bottom. Each tap can push a new screen onto the stack, and the back button pops the top screen off.

How SwiftUI stack navigation works
  1. Show root view
  2. User taps link
  3. Push destination
  4. Back pops view

SwiftUI navigation usually moves forward by pushing a destination and backward by removing it from the stack.

Because this topic naturally includes two related APIs, you should treat them differently:

2. Why SwiftUI Navigation Matters

Without navigation, most apps would be stuck on one screen. Real apps need users to move from summaries to details, from settings categories to specific settings, and from lists of items to full item pages.

Navigation matters because it helps you:

You should use SwiftUI navigation when a user moves between distinct screens. You should not use it for small visual changes that belong on the same screen, such as showing a loading spinner, expanding a section, or toggling a panel.

3. Basic Syntax or Core Idea

The core idea is simple: wrap your content in a navigation container, then use NavigationLink to move to another view.

Using NavigationStack

This is the modern basic pattern.

import SwiftUI

struct HomeView: View {
    var body: some View {
        NavigationStack {
            VStack(spacing: 16) {
                Text("Home Screen")

                NavigationLink("Show Detail") {
                    Text("Detail Screen")
                        .navigationTitle("Detail")
                }
            }
            .padding()
            .navigationTitle("Home")
        }
    }
}

This example creates a root screen with a title and a link. Tapping the link pushes the detail screen onto the navigation stack.

Using NavigationView

You will still see this in many tutorials and older projects.

import SwiftUI

struct LegacyHomeView: View {
    var body: some View {
        NavigationView {
            NavigationLink("Show Detail") {
                Text("Detail Screen")
                    .navigationTitle("Detail")
            }
            .navigationTitle("Home")
        }
    }
}

This older pattern still works in many cases, but it is not the preferred API for new SwiftUI navigation code.

The core pieces

4. Step-by-Step Examples

Example 1: Simple push navigation

Start with the most common beginner case: one screen links to another.

import SwiftUI

struct SimpleNavigationView: View {
    var body: some View {
        NavigationStack {
            VStack(spacing: 20) {
                Text("Welcome to the app")

                NavigationLink("Go to Profile") {
                    Text("Profile Screen")
                        .navigationTitle("Profile")
                }
            }
            .padding()
            .navigationTitle("Home")
        }
    }
}

This is the basic push-and-back experience. The system automatically shows a back button on the destination screen.

Example 2: Navigation from a list

Lists are one of the most common places to use navigation.

import SwiftUI

struct FruitListView: View {
    let fruits = ["Apple", "Banana", "Orange"]

    var body: some View {
        NavigationStack {
            List(fruits, id: \.self) { fruit in
                NavigationLink(fruit) {
                    Text("You selected \(fruit)")
                        .navigationTitle(fruit)
                }
            }
            .navigationTitle("Fruits")
        }
    }
}

Here, each list row opens a matching detail screen. This pattern appears in contacts, messages, products, settings categories, and many other app features.

Example 3: Value-based navigation with navigationDestination

NavigationStack supports a more scalable style where you navigate using values instead of directly embedding each destination view inside every link.

import SwiftUI

struct DestinationListView: View {
    let numbers = [1, 2, 3]

    var body: some View {
        NavigationStack {
            List(numbers, id: \.self) { number in
                NavigationLink("Open item \(number)", value: number)
            }
            .navigationTitle("Numbers")
            .navigationDestination(for: Int.self) { number in
                Text("Detail for item \(number)")
                    .navigationTitle("Item \(number)")
            }
        }
    }
}

This approach is especially useful when many links of the same value type should go to the same kind of destination.

Example 4: Programmatic navigation with a path

Sometimes a button or app event should navigate without the user tapping a visible NavigationLink. A path lets you push values in code.

import SwiftUI

struct PathNavigationView: View {
    @State private var path = NavigationPath()

    var body: some View {
        NavigationStack(path: $path) {
            VStack(spacing: 16) {
                Text("Dashboard")

                Button("Open Settings") {
                    path.append("settings")
                }
            }
            .navigationTitle("Dashboard")
            .navigationDestination(for: String.self) { route in
                if route == "settings" {
                    Text("Settings Screen")
                        .navigationTitle("Settings")
                }
            }
        }
    }
}

This shows programmatic navigation. The button appends a value to the path, and SwiftUI uses the destination mapping to display the correct screen.

5. Practical Use Cases

6. Common Mistakes

Mistake 1: Using NavigationView for all new code

Many beginners copy older examples and assume NavigationView is still the best default. It often works, but it does not provide the same modern, value-based navigation model as NavigationStack.

Problem: This code uses the legacy navigation container in a new app, which can make future navigation logic harder to scale and maintain.

NavigationView {
    List {
        NavigationLink("Profile") {
            Text("Profile Screen")
        }
    }
}

Fix: Prefer NavigationStack in modern SwiftUI code unless you are maintaining older APIs or supporting code written around the older container.

NavigationStack {
    List {
        NavigationLink("Profile") {
            Text("Profile Screen")
        }
    }
}

The corrected version uses the modern navigation API and is better suited to current SwiftUI patterns.

Mistake 2: Creating a NavigationLink without a navigation container

NavigationLink needs to live inside a navigation container. Without NavigationStack or NavigationView, the navigation behavior will not work as expected.

Problem: This link is not inside a navigation container, so the app cannot present it as part of a navigation stack.

struct ContentView: View {
    var body: some View {
        NavigationLink("Open Detail") {
            Text("Detail")
        }
    }
}

Fix: Place the link inside a navigation container so SwiftUI can manage the stack and back navigation.

struct ContentView: View {
    var body: some View {
        NavigationStack {
            NavigationLink("Open Detail") {
                Text("Detail")
            }
            .navigationTitle("Home")
        }
    }
}

The corrected version works because the link now belongs to a real navigation hierarchy.

Mistake 3: Using value-based NavigationLink without a matching navigationDestination

When you use NavigationLink(value:), SwiftUI needs a matching navigationDestination(for:) for that value type.

Problem: This code sends an Int value into the navigation stack, but no destination mapping tells SwiftUI how to display that type.

NavigationStack {
    NavigationLink("Open Item", value: 5)
}

Fix: Add a matching destination handler for the same value type.

NavigationStack {
    NavigationLink("Open Item", value: 5)
        .navigationTitle("Home")
}
.navigationDestination(for: Int.self) { number in
    Text("Item \(number)")
}

The corrected version works because SwiftUI now knows how to turn an Int in the stack into a destination view.

Mistake 4: Putting the title on the wrong view

Beginners often expect the navigation title to appear no matter where it is placed, but modifier placement matters in SwiftUI.

Problem: This title is attached to a child view in a way that may not represent the intended screen title clearly.

NavigationStack {
    VStack {
        Text("Dashboard")
            .navigationTitle("Home")
    }
}

Fix: Attach the title to the main container view for that screen so the ownership is clearer and easier to maintain.

NavigationStack {
    VStack {
        Text("Dashboard")
    }
    .navigationTitle("Home")
}

The corrected version makes it clearer which screen owns the title and avoids confusion as the layout grows.

7. Best Practices

Practice 1: Prefer NavigationStack for new apps

The newer API is easier to scale, especially when you need value-based navigation or path control.

// Preferred for modern SwiftUI navigation
NavigationStack {
    Text("Home")
}

This keeps your code aligned with current SwiftUI navigation patterns.

Practice 2: Use value-based navigation for repeated destination types

If many links open the same kind of destination, centralizing the destination mapping is cleaner than embedding destination views everywhere.

NavigationStack {
    List(1...3, id: \.self) { item in
        NavigationLink("Item \(item)", value: item)
    }
    .navigationDestination(for: Int.self) { item in
        Text("Detail for \(item)")
    }
}

This reduces repeated code and makes navigation rules easier to understand.

Practice 3: Keep route values simple and predictable

When using a path, simple route values are easier to test and reason about than unclear mixed data.

@State private var path = NavigationPath()

Button("Open Settings") {
    path.append("settings")
}

Simple route values make programmatic navigation easier to debug.

Practice 4: Set titles and toolbars per screen

Each screen should clearly describe itself. Users rely on titles and navigation bar items to understand where they are.

Text("Settings Screen")
    .navigationTitle("Settings")
    .toolbar {
        ToolbarItem(placement: .topBarTrailing) {
            Button("Save") {
            }
        }
    }

This makes the destination screen feel complete and easier to use.

8. Limitations and Edge Cases

9. NavigationView vs NavigationStack

This comparison is important because developers frequently see both APIs and need to know which one to choose.

FeatureNavigationViewNavigationStack
SwiftUI eraOlder APIModern API
Recommended for new appsNo, usually notYes
Value-based navigationLimited pattern supportBuilt for it
Programmatic path controlLess clearStrong support
Common in older tutorialsVery commonIncreasingly common

When to use NavigationView

When to use NavigationStack

A good rule is simple: learn both, but choose NavigationStack first when building modern SwiftUI apps.

NavigationView vs NavigationStack
NavigationView
  • Older API
  • Common in legacy code
  • Less flexible patterns
NavigationStack
  • Modern API
  • Path-based navigation
  • Preferred for new apps

Most new SwiftUI navigation code should start with NavigationStack.

10. Practical Mini Project

This mini project builds a small app screen with a list of topics and a detail view for each one using modern value-based navigation.

import SwiftUI

struct TopicListView: View {
    let topics = ["Basics", "State", "Navigation", "Lists"]

    var body: some View {
        NavigationStack {
            List(topics, id: \.self) { topic in
                NavigationLink(topic, value: topic)
            }
            .navigationTitle("SwiftUI Topics")
            .navigationDestination(for: String.self) { topic in
                TopicDetailView(topic: topic)
            }
        }
    }
}

struct TopicDetailView: View {
    let topic: String

    var body: some View {
        VStack(spacing: 16) {
            Text("Topic: \(topic)")
                .font(.title2)

            Text("This screen shows details for the selected topic.")
                .multilineTextAlignment(.center)
                .padding(.horizontal)
        }
        .padding()
        .navigationTitle(topic)
    }
}

This mini project demonstrates a realistic pattern: a root list pushes a typed value, and one destination rule maps that value to a detail view. This scales better than embedding a separate destination closure in every row for large lists.

11. Key Points

12. Practice Exercise

Build a small two-screen SwiftUI app that starts with a list of three cities. When the user taps a city, show a detail screen with the city name as the navigation title.

Expected output: A root screen titled Cities and a detail screen for the selected city.

Hint: Use NavigationLink(value:) in the list and navigationDestination(for:) on the stack.

import SwiftUI

struct CityListView: View {
    let cities = ["London", "Tokyo", "Sydney"]

    var body: some View {
        NavigationStack {
            List(cities, id: \.self) { city in
                NavigationLink(city, value: city)
            }
            .navigationTitle("Cities")
            .navigationDestination(for: String.self) { city in
                CityDetailView(city: city)
            }
        }
    }
}

struct CityDetailView: View {
    let city: String

    var body: some View {
        VStack {
            Text("Welcome to \(city)")
                .font(.title)
        }
        .navigationTitle(city)
    }
}

13. Final Summary

SwiftUI navigation gives your app structure by letting users move between screens in a familiar way. The two names you will see most often are NavigationView and NavigationStack. While both are important to recognize, modern SwiftUI development should usually start with NavigationStack because it offers a cleaner model, better value-based navigation, and stronger support for programmatic routes.

In this article, you learned what SwiftUI navigation is, why it matters, how to create links and titles, how to use value-based destinations, how to control navigation with a path, and how to avoid common mistakes. A strong next step is to learn SwiftUI state management in more depth, especially @State, @Binding, and data flow patterns that drive navigation in larger apps.