All articles
iOS14 min read

SwiftUI Navigation: NavigationStack, Deep Linking, and TabView Explained

🧭 This guide demystifies SwiftUI’s navigation system — from TabView setups to deep linking and advanced NavigationPaths. No breadcrumbs…

K
Karan Pal
Author
Image generated by AI
Image generated by AI

🧭 Introduction: Why Navigation Still Confuses Everyone (Including Me)

If SwiftUI had a theme park, navigation would be the haunted house — exciting at first, but you quickly realize you don’t know how to get out. 😅

Apple gave us NavigationView, then gently tapped us on the shoulder with a shiny new NavigationStack. Throw in NavigationPath, onOpenURL, and everyone’s favorite mystery box — deep linking — and now even seasoned iOS devs are looking like that meme of the guy with the math equations floating around him.

But don’t worry. Whether you’re building a basic app with a few screens or designing a dynamic flow with URLs and custom data types, SwiftUI actually can make navigation easy. Once you understand the tools (and know when not to fight them), it all clicks.

Here’s what you’ll learn in this article:

Let’s get started with SwiftUI’s simplest navigation tool: **TabView** 🧱

🧱 TabView: SwiftUI’s Easiest Navigation Tool

If NavigationStack is the fancy new toy, TabView is the good ol’ bicycle you always go back to — simple, reliable, and surprisingly fun to ride 🚴‍♂️

What is **TabView**? It’s SwiftUI’s way of letting users switch between multiple sections of your app using a bottom tab bar — think Instagram's home, search, reels, profile... you get the idea.

Here’s how easy it is to get started:

struct ContentView: View {
    var body: some View {
        TabView {
            HomeView()
                .tabItem {
                    Label("Home", systemImage: "house.fill")
                }

            SettingsView()
                .tabItem {
                    Label("Settings", systemImage: "gear")
                }
        }
    }
}

Two views, two tabs, and a working app. No routers, no magic.

🛠️ Pro Tips:

😵‍💫 Common Confusion: TabView + NavigationStack

Yes, you can combine both — just place a NavigationStack inside each tab if you want to push screens within them. Like this:

TabView {
    NavigationStack {
        HomeView()
    }
    .tabItem { Label("Home", systemImage: "house") }

    NavigationStack {
        SettingsView()
    }
    .tabItem { Label("Settings", systemImage: "gear") }
}

And just like that, each tab has its own navigation context. 🎯

🧭 NavigationStack: Say Goodbye to NavigationView

Remember NavigationView? Well, Apple quietly showed it the door starting iOS 16 and welcomed NavigationStack as the new go-to for screen-to-screen navigation. And no, this isn't just a rename — it's a whole new mindset.

🧩 What is NavigationStack?

Think of it like a modern, more powerful version of NavigationView, built to work better with SwiftUI’s evolving data-driven nature. It allows you to push and pop views using value types, with more control over the navigation history.

Here’s a simple example:

struct ContentView: View {
    var body: some View {
        NavigationStack {
            List {
                NavigationLink("Go to Detail", value: "SwiftUI ❤️ NavigationStack")
            }
            .navigationDestination(for: String.self) { value in
                Text("You selected: \(value)")
            }
            .navigationTitle("Home")
        }
    }
}

✅ Key Concepts:

✨ When to Use It:

🧠 Small Shift, Big Impact:

One thing to note: with NavigationStack, you’re no longer tying navigation to view hierarchies. Instead, it’s more like building a stack of data values — and SwiftUI figures out how to display them.

🧭 How NavigationLink and .navigationDestination Work Together

In NavigationStack, navigation is driven by values, not by hardcoded destination views. This is very different from how NavigationView used to work. Let’s unpack it:

🔗 Step 1: You Use NavigationLink(value:) to Push a Value

Instead of writing something like NavigationLink(destination: DetailView()), you now push a value of a specific type:

NavigationLink("Go to Profile", value: "karanpal")

This line says:

“When tapped, push a view onto the navigation stack, and use the string value _"karanpal"_ to figure out what view to show.”

🧩 Step 2: You Define What to Do With That Value Using .navigationDestination(for:)

This is where you declare how a specific type should be handled when it’s pushed onto the stack:

.navigationDestination(for: String.self) { username in
    ProfileView(username: username)
}

This means:

“If a _String_ is pushed, use it to build this specific view.”

🎯 The Connection in Plain Words

🧵 NavigationPath: For When You Want Full Control

By now, NavigationLink(value:) and .navigationDestination(for:) probably feel like a neat little duo. But what if your app’s navigation gets more complex?

Maybe you’re:

That’s when NavigationPath steps in like:

“Hold my SwiftLint.”

🧩 What Is NavigationPath?

NavigationPath is a special SwiftUI object that acts as a type-erased stack of values. It lets you programmatically push, pop, and manipulate the stack — no user tap required.

Think of it as: 🧠 “I’ll track everything that has been navigated to, and I don’t care what type it is — just tell me how to render it.”

🔍 Simple Use Case

Let’s say we want to push different kinds of data — a User or an Article.

struct User: Hashable { let name: String }
struct Article: Hashable { let title: String }

Here’s how to set up a dynamic navigation path:

@State private var path = NavigationPath()

NavigationStack(path: $path) {
    List {
        Button("Open User") {
            path.append(User(name: "Karan"))
        }
        Button("Open Article") {
            path.append(Article(title: "SwiftUI Tips"))
        }
    }
    .navigationDestination(for: User.self) { user in
        Text("User: \(user.name)")
    }
    .navigationDestination(for: Article.self) { article in
        Text("Article: \(article.title)")
    }
}

💪 Why Use It?

With NavigationPath, you can:

🎯 Real-World Scenarios

NavigationPath is like giving SwiftUI a dynamic GPS instead of just following a static route. You control the stack. You push the values. And SwiftUI does the rendering.

Next up? Let’s explore Deep Linking — and finally make those myapp:// URLs work the way they should.

🔗 Deep Linking in SwiftUI: It’s Not as Scary as It Sounds

Ever tapped a notification or a link on a website and ended up deep inside an app, bypassing the splash screen like a VIP guest? That’s deep linking. And yes — SwiftUI supports it natively, no UIKit magic required. 🪄

🚪 What Is Deep Linking?

Deep linking is how your app opens a specific screen when triggered by a URL, like:

myapp://profile/karanpal
myapp://article/swiftui-navigation

It’s super useful for:

🧰 Handling Deep Links in SwiftUI: No AppDelegate Needed

In UIKit, this all went through AppDelegate. In SwiftUI, it’s handled using .onOpenURL {} — and here’s the key part:

_The_ **_.onOpenURL_** _should be placed at the top level of your app_, ideally inside the _App_ struct, to make sure it catches links no matter where the user is.

🛠️ Example: Handle a Profile Link

Let’s say you want to support:

myapp://profile/karanpal

Here’s how to wire it up in your SwiftUI App struct:

@main
struct MyApp: App {
    @State private var path = NavigationPath()

    var body: some Scene {
        WindowGroup {
            NavigationStack(path: $path) {
                HomeView()
                    .onOpenURL { url in
                        handleDeepLink(url)
                    }
                    .navigationDestination(for: User.self) { user in
                        ProfileView(username: user.name)
                    }
            }
        }
    }

    private func handleDeepLink(_ url: URL) {
        guard let host = url.host else { return }

        switch host {
        case "profile":
            if let username = url.pathComponents.dropFirst().first {
                path.append(User(name: username))
            }
        default:
            break
        }
    }
}

This setup ensures:

📌 Registering Your URL Scheme

To let iOS know your app handles these kinds of links:

  1. Go to your app’s .xcodeproj settings
  2. Target → Info → URL Types
  3. Add a new entry with your custom scheme (myapp)

Boom — your app is now link-friendly. ✅

💬 Want a Deep Dive on Deep Linking?

This was just the basics. If you’d like a full walkthrough on:

Drop a comment, and I’ll cook up a full article just for that 🍝

⚠️ Pro Tips and Gotchas: Stuff That’ll Save You Hours of Debugging

SwiftUI’s navigation system is sleek, modern, and declarative… until it suddenly isn’t. Here are some real-world gotchas (and how to dodge them like a Swift ninja 🥷).

🪤 1. NavigationLink Doesn’t Trigger? Check the value Type

If tapping a NavigationLink(value:) silently does nothing, it’s usually because:

Fix: Always ensure the value type and the .navigationDestination(for:) type match exactly — no Optional, no mismatches.

🌀 2. Nesting NavigationStack Inside Another? SwiftUI Might Get Confused

Trying to nest a NavigationStack inside another (like in a tab)? Technically allowed. Logically dangerous.

You might see:

Fix: Avoid nested stacks. Instead, create a new NavigationStack at the TabView level — each tab should own its own stack.

🧪 3. Manually Modifying path? It’s Easy to Break the Stack

While path.append(...) is powerful, blindly pushing values can:

Fix: Validate before appending. Also, if you’re restoring state from JSON or a deep link, rebuild the path in the correct order.

💥 4. Navigation State Doesn’t Reset on Logout

If you use NavigationPath and the user logs out, they might still be deep in the stack when they log back in.

Fix: On logout, call:

path = NavigationPath()

This resets the stack, returning the user to the start of your navigation flow. ✨

🫣 5. TabView + NavigationStack: Watch Out for State Confusion

Each tab can have its own NavigationStack, but if you use shared state across tabs, SwiftUI might preserve unexpected behavior (like going back to a deep screen after switching tabs).

Fix: Use separate view models per tab, or reset navigation state when the user switches tabs if needed.

🧶 Summary: You’ve Just Untangled SwiftUI’s Navigation Web

If you made it this far — congratulations! 🎉 You now understand more about SwiftUI navigation than most developers who rage-quit halfway through building a tab bar.

Here’s a quick recap of what we covered:

SwiftUI’s navigation model may feel like a bit of a maze at first, but once you learn the rules of the game, it’s both elegant and powerful.

💬 Got Questions or Suggestions?

Have a deep linking use case you’re stuck on? Confused about nested NavigationStacks? Drop a comment below — I’d love to hear your thoughts and help where I can. 👇

🧠 Hungry for More?

Now that you’ve mastered the fundamentals of SwiftUI navigation, you’re ready to tackle more advanced scenarios that real-world apps demand.

Ready for the next level? Check out my follow-up guide: **SwiftUI Navigation with Enums: Advanced Deep Linking and Navigation History**

In that deep-dive, you’ll learn how to:

Build type-safe navigation with enums that prevent impossible app states ✅ Implement smart back navigation — jump directly from screen D to screen B ✅ Handle complex deep linking — convert URLs directly to navigation destinations ✅ Manage navigation history with modern @Observable patterns ✅ Deal with real-world edge cases — authentication, missing content, malformed URLs

Perfect if you’re building apps with:

The techniques build directly on what you’ve learned here, taking your navigation architecture from “basic” to “production-ready.”

**SwiftUI Navigation with Enums: Advanced Deep Linking and Navigation History** _Build type-safe navigation that handles complex app flows, URL routing, and smart back navigation using modern SwiftUI…_medium.comhttps://medium.com/swift-pal/swiftui-navigation-with-enums-advanced-deep-linking-and-navigation-history-60a38fa2a4a9

🎉 Enjoyed this article? Your support means the world to me!

💬 Drop a comment below! I love hearing about your experiences and answering questions

🎬 Subscribe on Youtube and become early subscribers of my channel: https://www.youtube.com/@swift-pal

💼 Let’s connect on LinkedIn for more professional insights: https://www.linkedin.com/in/karan-pal

Happy coding! 🚀

● The newsletter

New articles, straight to your inbox.

No spam, no filler — just new writing on iOS, the web, and AI when it ships. Unsubscribe anytime.

Keep reading