SwiftUI
navigation
best practices
view design
code maintainability

SwiftUI - how to avoid navigation hardcoded into the view?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Hardcoded navigation makes SwiftUI views harder to reuse because the view decides not only what it displays, but also where the app goes next. A better design is to treat navigation as state or intent owned by a parent, router, or coordinator so child views stay focused on UI and user actions.

Why Hardcoded Navigation Becomes a Problem

A view such as this is tightly coupled:

swift
NavigationLink("Open Details", destination: DetailsView())

That is fine for a quick prototype, but it becomes awkward when:

  • the destination changes by feature flag or user role
  • the same button appears in multiple flows
  • navigation must be triggered after async work
  • you want to test the view without bringing the full destination graph along

The deeper issue is ownership. The child view knows too much about the app's navigation structure.

Pass Intent Upward Instead of Embedding Destinations

A simple first improvement is to make the child view emit an action and let the parent decide what navigation should happen.

swift
1import SwiftUI
2
3struct ProfileCard: View {
4    let onOpenDetails: () -> Void
5
6    var body: some View {
7        Button("Open Details") {
8            onOpenDetails()
9        }
10    }
11}

Now the parent owns the navigation:

swift
1struct HomeView: View {
2    @State private var showDetails = false
3
4    var body: some View {
5        NavigationStack {
6            ProfileCard {
7                showDetails = true
8            }
9            .navigationDestination(isPresented: $showDetails) {
10                DetailsView()
11            }
12        }
13    }
14}

This small change already makes ProfileCard reusable outside this one flow.

Model Navigation as Route State

For more complex apps, use explicit route state rather than many independent booleans.

swift
1enum Route: Hashable {
2    case details
3    case settings
4    case user(id: Int)
5}

Then manage a path in the parent:

swift
1struct RootView: View {
2    @State private var path: [Route] = []
3
4    var body: some View {
5        NavigationStack(path: $path) {
6            VStack {
7                Button("Open Settings") {
8                    path.append(.settings)
9                }
10                Button("Open User 42") {
11                    path.append(.user(id: 42))
12                }
13            }
14            .navigationDestination(for: Route.self) { route in
15                switch route {
16                case .details:
17                    DetailsView()
18                case .settings:
19                    SettingsView()
20                case .user(let id):
21                    UserView(userID: id)
22                }
23            }
24        }
25    }
26}

This scales much better than embedding NavigationLink destinations directly into many leaf views.

Environment-Based Routers Can Help

If many distant views need to trigger navigation, a router object can reduce repeated path plumbing.

swift
1final class Router: ObservableObject {
2    @Published var path: [Route] = []
3
4    func push(_ route: Route) {
5        path.append(route)
6    }
7}

Inject it at the root:

swift
1struct AppRoot: View {
2    @StateObject private var router = Router()
3
4    var body: some View {
5        NavigationStack(path: $router.path) {
6            ContentView()
7                .environmentObject(router)
8                .navigationDestination(for: Route.self) { route in
9                    switch route {
10                    case .details:
11                        DetailsView()
12                    case .settings:
13                        SettingsView()
14                    case .user(let id):
15                        UserView(userID: id)
16                    }
17                }
18        }
19    }
20}

And use it in child views:

swift
1struct ContentView: View {
2    @EnvironmentObject private var router: Router
3
4    var body: some View {
5        Button("Go to Details") {
6            router.push(.details)
7        }
8    }
9}

This keeps navigation centralized while still letting deep views express intent.

Avoid Putting Business Logic in the Router

A router should know about routes, not business decisions. If a navigation action depends on loading data or checking permissions, that work should usually happen in a view model or parent feature layer first, then the route is emitted afterward.

That separation prevents the router from turning into a giant global decision object.

The goal is not to ban NavigationLink. It is to use it where the destination is genuinely a local UI concern.

For example, a simple static settings screen can still use:

swift
NavigationLink("About", destination: AboutView())

The architecture only becomes a problem when hardcoded navigation prevents reuse or couples views to flows they should not own.

Common Pitfalls

  • Embedding destination views directly inside reusable leaf components.
  • Replacing one hardcoded NavigationLink problem with a giant all-knowing global router.
  • Using many separate boolean flags when a route enum would describe the navigation state more clearly.
  • Letting child views decide business rules and navigation at the same time.
  • Avoiding all local NavigationLink usage even when the destination is genuinely simple and stable.

Summary

  • Hardcoded navigation makes SwiftUI views less reusable and harder to test.
  • A better pattern is to lift navigation state or navigation intent to a parent, router, or coordinator layer.
  • 'NavigationStack with a route enum scales better than many embedded destination definitions.'
  • Child views should usually emit intent, not own app-level routing decisions.
  • Use direct NavigationLink only when the navigation is truly local and stable.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.