navigation-patterns

Navigation Patterns — Expert Decisions

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "navigation-patterns" with this command: npx skills add kaakati/rails-enterprise-dev/kaakati-rails-enterprise-dev-navigation-patterns

Navigation Patterns — Expert Decisions

Expert decision frameworks for SwiftUI navigation choices. Claude knows NavigationStack syntax — this skill provides judgment calls for architecture decisions and state management trade-offs.

Decision Trees

Navigation Architecture Selection

How complex is your navigation? ├─ Simple (linear flows, 1-3 screens) │ └─ NavigationStack with inline NavigationLink │ No Router needed │ ├─ Medium (multiple flows, deep linking required) │ └─ NavigationStack + Router (ObservableObject) │ Centralized navigation state │ └─ Complex (tabs with independent stacks, cross-tab navigation) └─ Tab Coordinator + per-tab Routers Each tab maintains own NavigationPath

NavigationPath vs Typed Array

Do you need heterogeneous routes? ├─ YES (different types in same stack) │ └─ NavigationPath (type-erased) │ path.append(User(...)) │ path.append(Product(...)) │ └─ NO (single route enum) └─ @State var path: [Route] = [] Type-safe, debuggable, serializable

Rule: Prefer typed arrays unless you genuinely need mixed types. NavigationPath's type erasure makes debugging harder.

Deep Link Handling Strategy

When does deep link arrive? ├─ App already running (warm start) │ └─ Direct navigation via Router │ └─ App launches from deep link (cold start) └─ Is view hierarchy ready? ├─ YES → Navigate immediately └─ NO → Queue pending deep link Handle in root view's .onAppear

Modal vs Push Selection

Is the destination a self-contained flow? ├─ YES (can complete/cancel independently) │ └─ Modal (.sheet or .fullScreenCover) │ Examples: Settings, Compose, Login │ └─ NO (part of current navigation hierarchy) └─ Push (NavigationLink or path.append) Examples: Detail views, drill-down

NEVER Do

NavigationPath State

NEVER store NavigationPath in ViewModel without careful consideration:

// ❌ ViewModel owns navigation — couples business logic to navigation @MainActor final class HomeViewModel: ObservableObject { @Published var path = NavigationPath() // Wrong layer! }

// ✅ Router/Coordinator owns navigation, ViewModel owns data @MainActor final class Router: ObservableObject { @Published var path = NavigationPath() }

@MainActor final class HomeViewModel: ObservableObject { @Published var items: [Item] = [] // Data only }

NEVER use NavigationPath across tabs:

// ❌ Shared path across tabs — navigation becomes unpredictable struct MainTabView: View { @StateObject var router = Router() // Single router!

var body: some View {
    TabView {
        // Both tabs share same path — chaos
    }
}

}

// ✅ Each tab has independent navigation stack struct MainTabView: View { @StateObject var homeRouter = Router() @StateObject var searchRouter = Router()

var body: some View {
    TabView {
        NavigationStack(path: $homeRouter.path) { ... }
        NavigationStack(path: $searchRouter.path) { ... }
    }
}

}

NEVER forget to handle deep links arriving before view hierarchy:

// ❌ Race condition — navigation may fail silently @main struct MyApp: App { @StateObject var router = Router()

var body: some Scene {
    WindowGroup {
        ContentView()
            .onOpenURL { url in
                router.handle(url)  // View may not exist yet!
            }
    }
}

}

// ✅ Queue deep link for deferred handling @main struct MyApp: App { @StateObject var router = Router() @State private var pendingDeepLink: URL?

var body: some Scene {
    WindowGroup {
        ContentView()
            .onAppear {
                if let url = pendingDeepLink {
                    router.handle(url)
                    pendingDeepLink = nil
                }
            }
            .onOpenURL { url in
                pendingDeepLink = url
            }
    }
}

}

Route Design

NEVER use stringly-typed routes:

// ❌ No compile-time safety, typos cause runtime failures func navigate(to screen: String) { switch screen { case "profile": ... case "setings": ... // Typo — silent failure } }

// ✅ Enum routes with associated values enum Route: Hashable { case profile(userId: String) case settings }

NEVER put navigation logic in Views:

// ❌ View knows too much about app structure struct ItemRow: View { var body: some View { NavigationLink { ItemDetailView(item: item) // View creates destination } label: { Text(item.name) } } }

// ✅ Delegate navigation to Router struct ItemRow: View { @EnvironmentObject var router: Router

var body: some View {
    Button(item.name) {
        router.navigate(to: .itemDetail(item.id))
    }
}

}

Navigation State Persistence

NEVER lose navigation state on app termination without consideration:

// ❌ User loses their place when app is killed @StateObject var router = Router() // State lost on terminate

// ✅ Persist for important flows (optional based on UX needs) @SceneStorage("navigationPath") private var pathData: Data?

var body: some View { NavigationStack(path: $router.path) { ... } .onAppear { router.restore(from: pathData) } .onChange(of: router.path) { pathData = router.serialize() } }

Essential Patterns

Type-Safe Router

@MainActor final class Router: ObservableObject { enum Route: Hashable { case userList case userDetail(userId: String) case settings case settingsSection(SettingsSection) }

@Published var path: [Route] = []

func navigate(to route: Route) {
    path.append(route)
}

func pop() {
    guard !path.isEmpty else { return }
    path.removeLast()
}

func popToRoot() {
    path.removeAll()
}

func replaceStack(with routes: [Route]) {
    path = routes
}

@ViewBuilder
func destination(for route: Route) -> some View {
    switch route {
    case .userList:
        UserListView()
    case .userDetail(let userId):
        UserDetailView(userId: userId)
    case .settings:
        SettingsView()
    case .settingsSection(let section):
        SettingsSectionView(section: section)
    }
}

}

Deep Link Handler

enum DeepLink { case user(id: String) case product(id: String) case settings

init?(url: URL) {
    guard let scheme = url.scheme,
          ["myapp", "https"].contains(scheme) else { return nil }

    let path = url.path.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
    let components = path.components(separatedBy: "/")

    switch components.first {
    case "user":
        guard components.count > 1 else { return nil }
        self = .user(id: components[1])
    case "product":
        guard components.count > 1 else { return nil }
        self = .product(id: components[1])
    case "settings":
        self = .settings
    default:
        return nil
    }
}

}

extension Router { func handle(_ deepLink: DeepLink) { popToRoot()

    switch deepLink {
    case .user(let id):
        navigate(to: .userList)
        navigate(to: .userDetail(userId: id))
    case .product(let id):
        navigate(to: .productDetail(productId: id))
    case .settings:
        navigate(to: .settings)
    }
}

}

Tab + Navigation Coordination

struct MainTabView: View { @State private var selectedTab: Tab = .home @StateObject private var homeRouter = Router() @StateObject private var profileRouter = Router()

enum Tab { case home, search, profile }

var body: some View {
    TabView(selection: $selectedTab) {
        NavigationStack(path: $homeRouter.path) {
            HomeView()
                .navigationDestination(for: Router.Route.self) { route in
                    homeRouter.destination(for: route)
                }
        }
        .tag(Tab.home)
        .environmentObject(homeRouter)

        NavigationStack(path: $profileRouter.path) {
            ProfileView()
                .navigationDestination(for: Router.Route.self) { route in
                    profileRouter.destination(for: route)
                }
        }
        .tag(Tab.profile)
        .environmentObject(profileRouter)
    }
}

// Pop to root on tab re-selection
func tabSelected(_ tab: Tab) {
    if selectedTab == tab {
        switch tab {
        case .home: homeRouter.popToRoot()
        case .profile: profileRouter.popToRoot()
        case .search: break
        }
    }
    selectedTab = tab
}

}

Quick Reference

Navigation Architecture Comparison

Pattern Complexity Deep Link Support Testability

Inline NavigationLink Low Manual Low

Router with typed array Medium Good High

NavigationPath Medium Good Medium

Coordinator Pattern High Excellent Excellent

When to Use Each Modal Type

Modal Type Use For

.sheet

Secondary tasks, can dismiss

.fullScreenCover

Immersive flows (onboarding, login)

.alert

Critical decisions

.confirmationDialog

Action choices

Red Flags

Smell Problem Fix

NavigationPath across tabs State confusion Per-tab routers

View creates destination directly Tight coupling Router pattern

String-based routing No compile safety Enum routes

Deep link ignored on cold start Race condition Pending URL queue

ViewModel owns NavigationPath Layer violation Router owns navigation

No popToRoot on tab re-tap UX expectation Handle tab selection

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

flutter conventions & best practices

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

getx state management patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

ruby oop patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

rails localization (i18n) - english & arabic

No summary provided by upstream source.

Repository SourceNeeds Review