Posted in

SwiftUI Navigation: A Beginner’s Guide with Real-World Example

SwiftUI navigation
Spread the love
Reading Time: 3 minutes

SwiftUI has evolved a lot in the way we handle navigation. If you’ve worked with UIKit, you probably remember using UINavigationController and pushing view controllers manually. In SwiftUI, things are more declarative — but also slightly confusing with multiple options like NavigationLink, NavigationStack, and navigationDestination.

In this post, we’ll walk through these concepts step by step.

1. The Basics: NavigationStack and NavigationLink

The simplest way to navigate in SwiftUI is by using NavigationStack and NavigationLink.

struct SettingsView: View {
    var body: some View {
        NavigationStack {
            List {
                NavigationLink("Profile", destination: ProfileView())
                NavigationLink("Security", destination: SecurityView())
                NavigationLink("Notifications", destination: NotificationsView())
                NavigationLink("Appearance", destination: AppearanceView())
                NavigationLink("Help", destination: HelpView())
            }
            .navigationTitle("Settings")
        }
    }
}

Here:

  • NavigationStack acts like a container (similar to UINavigationController in UIKit).
  • Each NavigationLink knows its destination directly.

👉 This approach is perfect for static, predictable screens like a settings page.

2. navigationDestination(for:) — When and Why?

Now, you might wonder: If NavigationLink(destination:) already works, why do we need navigationDestination(for:)?

The answer: It’s useful when your navigation is dynamic or programmatic.

Example: Enum-Based Navigation

enum SettingType: Hashable {
    case profile, security, notifications, appearance, help
}

struct SettingsView: View {
    let settings: [SettingType] = [.profile, .security, .notifications, .appearance, .help]
    
    var body: some View {
        NavigationStack {
            List(settings, id: \.self) { type in
                NavigationLink(type.label, value: type)
            }
            .navigationTitle("Settings")
            .navigationDestination(for: SettingType.self) { type in
                switch type {
                case .profile: ProfileView()
                case .security: SecurityView()
                case .notifications: NotificationsView()
                case .appearance: AppearanceView()
                case .help: HelpView()
                }
            }
        }
    }
}

extension SettingType {
    var label: String {
        switch self {
        case .profile: "Profile"
        case .security: "Security"
        case .notifications: "Notifications"
        case .appearance: "Appearance"
        case .help: "Help"
        }
    }
}

Here, we don’t hardcode destinations in every NavigationLink. Instead, we define a mapping between SettingType and its view in one place (navigationDestination).

This is cleaner and scalable, especially when your list is dynamic (e.g., items fetched from a server).

3. Programmatic Navigation with NavigationPath

Another superpower of navigationDestination is programmatic navigation.
For example: after login, you might want to automatically push the user to the Profile screen.

struct ContentView: View {
    @State private var path = NavigationPath()
    @State private var isLoggedIn = false
    
    var body: some View {
        NavigationStack(path: $path) {
            VStack {
                if isLoggedIn {
                    Text("Welcome back!")
                        .onAppear {
                            path.append(SettingType.profile) // Go to Profile programmatically
                        }
                } else {
                    Button("Login") {
                        isLoggedIn = true
                    }
                }
            }
            .navigationDestination(for: SettingType.self) { type in
                switch type {
                case .profile: ProfileView()
                case .security: SecurityView()
                case .notifications: NotificationsView()
                case .appearance: AppearanceView()
                case .help: HelpView()
                }
            }
        }
    }
}

Here’s what happens:

  • When the user logs in, we append .profile into the path.
  • SwiftUI automatically pushes the Profile screen without the user tapping anything.

👉 This is very handy for flows like login, onboarding, or deep linking.

4. When to Use What?

  • Simple NavigationLink(destination:)
    Use it when your destinations are static and few (like a settings screen).
  • navigationDestination(for:) with enum or model
    Use it when your destinations are dynamic, or you want to manage them in one place.
  • NavigationPath with programmatic navigation
    Use it when you need to navigate automatically (after login, from a push notification, or via deep link).

Final Thoughts

SwiftUI’s navigation APIs can feel a bit overwhelming at first, but once you understand the use cases, it becomes very powerful:

  • NavigationLink(destination:) = simple, static navigation.
  • navigationDestination(for:) = dynamic, decoupled navigation.
  • NavigationPath = programmatic navigation.

Start simple, and move to the advanced approach only when your app really needs it.

Hi, I’m Mumthasir, the creator of iOSTutor.com.

With over 11 years of experience in software development — including 8 years focused on iOS application development — I’ve had the opportunity to build and contribute to a wide range of applications, from e-commerce and education to healthcare and government platforms.

When I’m not coding, I enjoy exploring new technologies and writing content — from technical guides to stories and poems — with the hope that it might help or inspire someone, somewhere.

Leave a Reply

Your email address will not be published. Required fields are marked *