DEV Community

Sebastien Lato
Sebastien Lato

Posted on

Modern Navigation in SwiftUI

Navigation in SwiftUI has evolved massively since the early days of NavigationView.

With NavigationStack, NavigationPath, sheet detents, and modern transitions, we finally have a system that’s flexible and robust enough for real apps.

But many developers still struggle with:

  • programmatic navigation
  • multiple stacks (tabs + navigation)
  • sheets vs fullScreenCover
  • deep linking
  • passing data between screens
  • resetting stacks
  • best architecture practices

This guide will give you the cleanest, most scalable navigation patterns for SwiftUI.


🎯 1. Use NavigationStack Everywhere (Not NavigationView)

NavigationStack {
    HomeView()
}
Enter fullscreen mode Exit fullscreen mode

NavigationStack gives you:

  • programmatic navigation
  • deep-link support
  • custom stacks per tab
  • better back behavior
  • predictable state

πŸ“¦ 2. The Cleanest Way to Push New Screens

For simple apps:

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

    var body: some View {
        NavigationStack(path: $path) {
            VStack {
                Button("Open Details") {
                    path.append("details")
                }
            }
            .navigationDestination(for: String.self) { value in
                if value == "details" {
                    DetailsView()
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Why this works well:

  • path represents your full navigation history
  • type-safe destinations
  • easy to wipe/reset the stack

🧭 3. Deep Links & Programmatic Jumps

You can push multiple screens in one go:

path = ["profile", "settings", "advanced"]
Enter fullscreen mode Exit fullscreen mode

Great for deep links and onboarding.


πŸ—‚ 4. Navigation Per Tab (The Correct Architecture)

Each tab maintains its own stack.

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

            NavigationStack { ExploreView() }
                .tabItem { Label("Explore", systemImage: "sparkles") }

            NavigationStack { SettingsView() }
                .tabItem { Label("Settings", systemImage: "gear") }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This avoids all the classic problems:

  • tab switching resets screens (fixed)
  • navigation becomes global (fixed)
  • deep linking breaks tabs (fixed)

Each tab has its OWN history β€” just like Apple’s apps.


πŸͺŸ 5. Sheets vs Full Screen Covers (When to Use Each)

Use sheet when:

  • user can dismiss anytime
  • you want a card/detent look
  • modal feels lightweight
  • controls/settings/pickers
.sheet(isPresented: $showSheet) {
    SettingsSheet()
}
Enter fullscreen mode Exit fullscreen mode

Use fullScreenCover when:

  • experience is immersive
  • onboarding flows
  • media players
  • authentication
.fullScreenCover(isPresented: $showFull) {
    OnboardingFlow()
}
Enter fullscreen mode Exit fullscreen mode

This distinction matches Apple’s Human Interface Guidelines.


🧱 6. Sheet Detents

.sheet(isPresented: $show) {
    MySheet()
        .presentationDetents([.medium, .large])
        .presentationCornerRadius(22)
}
Enter fullscreen mode Exit fullscreen mode

Useful for:

  • filters
  • details panels
  • quick actions
  • maps
  • music player mini-panels

πŸ”— 7. Passing Data the RIGHT Way

Push with data:

path.append(UserDetailRoute(user))
Enter fullscreen mode Exit fullscreen mode

Destination:

.navigationDestination(for: UserDetailRoute.self) { route in
    UserDetailView(user: route.user)
}
Enter fullscreen mode Exit fullscreen mode

Much cleaner than environment objects everywhere.


πŸ“Œ 8. Resetting the Stack (Pop to Root)

withAnimation {
    path = NavigationPath()
}
Enter fullscreen mode Exit fullscreen mode

Perfect for:

  • logging out
  • finishing onboarding
  • resetting flows

🧠 9. Recommended Folder Structure for Navigation

Features/
β”‚
β”œβ”€β”€ Home/
β”œβ”€β”€ Explore/
β”œβ”€β”€ Settings/
β”‚
Navigation/
β”œβ”€β”€ Router.swift
β”œβ”€β”€ Routes.swift
└── NavigationModel.swift
Enter fullscreen mode Exit fullscreen mode

Keep routes centralized β€” not scattered across screens.


🧭 10. You NEED a Router (Scalable Architecture)

Create a central routing layer:

@Observable
class Router {
    var path = NavigationPath()

    func push(_ route: Route) {
        path.append(route)
    }

    func pop() {
        path.removeLast()
    }

    func popToRoot() {
        path = NavigationPath()
    }
}
Enter fullscreen mode Exit fullscreen mode

Inject it into screens that need navigation.

This scales apps from 5 screens to 50.


πŸš€ Final Thoughts

SwiftUI navigation is finally powerful, stable, and scalable β€” if you use it correctly.

Now you can:

  • push & pop cleanly
  • deep link properly
  • maintain separate stacks per tab
  • use sheets & full screens the right way
  • build with a routing architecture
  • manage complex flows

Top comments (0)