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…

🧭 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:
- How to use
TabViewfor quick multi-screen navigation - How
NavigationStackreplaces the old-schoolNavigationView - What the heck
NavigationPathdoes and why it exists - How to handle deep links and make your app respond to URLs
- Common pitfalls and how not to break your navigation logic in weird ways
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:
- Use
Label(title:image:)to combine icon + text nicely. - SF Symbols make your tabs feel Apple-native.
- SwiftUI keeps the state of each tab alive. So if your tab has a
@Statecounter and you switch away — it remembers! Handy or annoying depending on your design. - Want to hide the tab bar? Wrap
TabViewin a custom condition, but don’t try to force-hide it per screen — SwiftUI might fight back.
😵💫 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:
NavigationStackreplacesNavigationViewstarting from iOS 16.- Use
NavigationLink(value:)withnavigationDestination(for:)to create type-safe, dynamic navigation. - You define what type is being passed, and how it should be rendered when pushed.
✨ When to Use It:
- You want deep control over how navigation is handled.
- You’re passing data (like a model or string) to the next screen.
- You need a predictable and declarative way to push views — without the weird bugs
NavigationViewwas infamous for.
🧠 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.”- “Go to Profile” → This is the label that appears in the UI. It’s what the user sees and taps.
- “karanpal” → This is the value that gets pushed onto the
NavigationStackwhen tapped.
🧩 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
NavigationLink(value:)adds a value to theNavigationStack..navigationDestination(for:)tells SwiftUI how to turn that value into a view.- SwiftUI then automatically matches the pushed value’s type with the
.navigationDestinationclosure and shows the appropriate view.
🧵 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:
- Navigating between multiple types of destinations
- Building a custom backstack (yes, that’s a thing!)
- Restoring navigation state from saved data or a deep link
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:
- Navigate programmatically — push from logic, not just taps
- Handle multiple types in one stack
- Pop or reset the stack (
path.removeLast(),path = NavigationPath()) - Save/restore navigation history from state (great for deep linking or session persistence)
🎯 Real-World Scenarios
- Deep linking: You can build the path manually to match a URL like
myapp://user/Karan/article/SwiftUI - State restoration: When the user reopens the app, you can rebuild the path to return them to where they left off
- Onboarding flows: Programmatically push several views in a row when a condition is met
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-navigationIt’s super useful for:
- Opening the app from Safari or Mail
- Tapping push notification buttons
- Jumping between sections internally
🧰 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/karanpalHere’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:
- Your app listens for deep links as soon as it launches.
- You’re not depending on some random screen being active to catch the URL.
- You can push data-driven destinations using
NavigationPath.
📌 Registering Your URL Scheme
To let iOS know your app handles these kinds of links:
- Go to your app’s
.xcodeprojsettings - Target → Info → URL Types
- 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:
- Universal links
- Push notification routing
- Query parameter parsing
- Multi-screen path navigation from a URL
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:
- You didn’t define a
.navigationDestination(for:)for that exact type - Or you passed a non-Hashable/optional value
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:
- Weird navigation behavior
.navigationDestinationnot firing- The back button disappearing
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:
- Break your view hierarchy if
.navigationDestinationisn’t defined - Cause SwiftUI to silently ignore navigation
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:
- TabView: Easiest way to switch between app sections with minimal code and maximum vibe.
- NavigationStack: The modern replacement for
NavigationView, using value-driven navigation. - NavigationLink + .navigationDestination: A declarative duo — values trigger views, not imperative view pushes.
- NavigationPath: When you want to go pro. Programmatic navigation, dynamic stacks, and deep control.
- Deep Linking: Use
.onOpenURLat the app entry point, parse your URLs, and push values into the stack. - Gotchas: From weird back behavior to login-based state resets — you now know what to avoid and how to fix it.
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:
- Complex user flows (e-commerce, social apps, multi-step processes)
- Deep linking requirements (sharing specific screens, push notifications)
- Advanced navigation patterns (conditional flows, smart back buttons)
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! 🚀
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
Why I'm Rebuilding My Blog From Scratch (and Leaving Medium)
After years of publishing on someone else's platform, I'm moving my writing to a home I actually own. Here's the reasoning, and what I'm building instead.
ReadData Is the Model: The Most Ignored Part of AI
A beginner-friendly guide to why data quality beats model hype.
Read