100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Swift

NavigationStack Basics

Understand how NavigationStack replaced NavigationView, how navigationDestination drives push navigation, and how to structure hierarchical screens in SwiftUI.

NavigationIntermediate9 min readJul 8, 2026
Analogies

NavigationStack, introduced to replace the older NavigationView, is SwiftUI's container for presenting a stack of screens where each new screen is pushed on top of the previous one, mirroring the classic push/pop model familiar from UINavigationController. Unlike NavigationView's implicit, link-based navigation, NavigationStack separates 'what triggers navigation' from 'what data type maps to what destination view,' using navigationDestination(for:destination:) modifiers. This separation is what makes deep linking, state restoration, and programmatic navigation tractable in a way NavigationView never was.

🏏

Cricket analogy: Old-school scoring separated nothing — the scorer who noted a boundary was also the one deciding how it displayed; NavigationStack instead separates "a four was hit" from "how the graphic renders it," using a lookup table the same way navigationDestination(for:) maps data types to destination views.

Declaring Destinations by Data Type

Instead of wrapping each pushable row in a NavigationLink that directly embeds its destination view, NavigationStack lets you register a mapping from a data type to a destination view once, near the top of the stack. Any NavigationLink whose value matches that type will trigger that destination — this keeps row views lightweight (they just carry a value) and keeps the navigation logic centralized and easy to reason about.

🏏

Cricket analogy: A scorecard app doesn't need every row of the batting lineup to know how to render a player's full profile page — each row just carries the player's ID, and one central lookup near the top decides how to display any player, keeping row views lightweight.

swift
struct Recipe: Identifiable, Hashable {
    let id: UUID
    let name: String
}

struct RecipeListView: View {
    let recipes: [Recipe]

    var body: some View {
        NavigationStack {
            List(recipes) { recipe in
                NavigationLink(recipe.name, value: recipe)
            }
            .navigationTitle("Recipes")
            .navigationDestination(for: Recipe.self) { recipe in
                RecipeDetailView(recipe: recipe)
            }
        }
    }
}

struct RecipeDetailView: View {
    let recipe: Recipe

    var body: some View {
        Text(recipe.name)
            .navigationTitle(recipe.name)
    }
}

One NavigationStack Per Independent Flow

Each tab in a TabView, or each independently navigable region of your UI, generally needs its own NavigationStack, because a stack owns its own push/pop history. Nesting a NavigationStack inside another is almost always a mistake and leads to confusing or broken navigation bars; instead, place a single NavigationStack at the root of each flow and push all screens for that flow within it.

🏏

Cricket analogy: Each cricket tournament (IPL, Big Bash, The Hundred) keeps its own independent points table and history — you'd never nest one tournament's standings inside another's; likewise each tab needs its own NavigationStack, since nesting one inside another breaks the push/pop history.

Applying multiple navigationDestination(for:) modifiers for the SAME data type at different points in the view hierarchy, or forgetting to declare navigationDestination(for:) at all for a type you push, are frequent sources of 'nothing happens when I tap this row' bugs. The destination modifier must be reachable from where the NavigationLink lives, typically attached near the root of that stack.

Think of NavigationStack as a deck of index cards: the root view is the bottom card, and pushing a new screen puts a new card on top. navigationDestination(for:) is the rulebook that says 'if you draw a card of type Recipe, display it using RecipeDetailView' — the rulebook only needs to be written once for the whole deck.

  • NavigationStack replaces NavigationView and models navigation as an explicit stack of pushed screens.
  • navigationDestination(for:destination:) maps a Hashable data type to a destination view, decoupling triggers from destinations.
  • NavigationLink(value:) pushes a value onto the stack; the matching navigationDestination(for:) renders it.
  • Each independent navigable flow (e.g. each TabView tab) should have exactly one NavigationStack at its root.
  • Missing or duplicated navigationDestination(for:) declarations are the most common cause of broken pushes.
  • This value-driven model is what enables programmatic navigation and deep linking, covered in NavigationPath.

Practice what you learned

Was this page helpful?

Topics covered

#Swift#IOSWithSwiftUIStudyNotes#MobileDevelopment#NavigationStackBasics#NavigationStack#Declaring#Destinations#Data#StudyNotes#SkillVeris