Navigation Patterns

SwiftUI navigation approaches for different scenarios.

Problem

SwiftUI provides multiple navigation mechanisms (NavigationStack, sheets, fullScreenCover). Choosing the right approach impacts UX and code complexity.

Solution 1: NavigationStack (Hierarchical)

For drill-down navigation with back button:

struct ContentView: View {
    @State private var path = NavigationPath()
 
    var body: some View {
        NavigationStack(path: $path) {
            List {
                NavigationLink("Item 1", value: "item1")
                NavigationLink("Item 2", value: "item2")
            }
            .navigationDestination(for: String.self) { item in
                DetailView(item: item)
            }
        }
    }
}

Solution 2: fullScreenCover (Immersive)

For modal experiences without navigation chrome:

struct MainMenuView: View {
    @State private var showGame = false
 
    var body: some View {
        VStack {
            Button("Play") {
                showGame = true
            }
        }
        .fullScreenCover(isPresented: $showGame) {
            GameView(onExit: { showGame = false })
        }
    }
}

Solution 3: State-Driven Views

For contained flows with enum-based switching:

enum GamePhase {
    case menu
    case playing
    case result
}
 
struct GameContainer: View {
    @State private var phase: GamePhase = .menu
 
    var body: some View {
        switch phase {
        case .menu:
            MenuView(onStart: { phase = .playing })
        case .playing:
            PlayView(onComplete: { phase = .result })
        case .result:
            ResultView(onRestart: { phase = .menu })
        }
    }
}

Usage Patterns

Scene-Based Navigation (Quest-Forge)

struct StoryView: View {
    @State private var currentSceneId: String = "intro"
    let story: Story
 
    var body: some View {
        if let scene = story.scene(id: currentSceneId) {
            SceneView(scene: scene) { nextSceneId in
                withAnimation(.easeInOut(duration: 0.25)) {
                    currentSceneId = nextSceneId
                }
            }
        }
    }
}

Two-Mode Selection (MadLibs)

enum PlayMode {
    case classic
    case creative
}
 
struct ModeSelectionView: View {
    var onSelect: (PlayMode) -> Void
 
    var body: some View {
        HStack {
            Button("Classic") { onSelect(.classic) }
            Button("Creative") { onSelect(.creative) }
        }
    }
}

Trade-offs

  • Use when: Hierarchical content, need back button
  • Avoid when: Immersive experiences, game screens

fullScreenCover

  • Use when: Immersive modals, game screens
  • Avoid when: Quick interactions, drill-down content

State-Driven

  • Use when: Simple flows, contained experiences
  • Avoid when: Deep hierarchies, many destinations

Source

Extracted from: