スキル一覧に戻る
Kaakati

navigation-patterns

by Kaakati

Enterprise-grade Rails development workflow with multi-agent orchestration, beads task tracking, and quality gates. Generic and portable across any Rails project.

2🍴 0📅 2026年1月24日
GitHubで見るManusで実行

SKILL.md


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

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
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.

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
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

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))
        }
    }
}

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)
        }
    }
}
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

PatternComplexityDeep Link SupportTestability
Inline NavigationLinkLowManualLow
Router with typed arrayMediumGoodHigh
NavigationPathMediumGoodMedium
Coordinator PatternHighExcellentExcellent

When to Use Each Modal Type

Modal TypeUse For
.sheetSecondary tasks, can dismiss
.fullScreenCoverImmersive flows (onboarding, login)
.alertCritical decisions
.confirmationDialogAction choices

Red Flags

SmellProblemFix
NavigationPath across tabsState confusionPer-tab routers
View creates destination directlyTight couplingRouter pattern
String-based routingNo compile safetyEnum routes
Deep link ignored on cold startRace conditionPending URL queue
ViewModel owns NavigationPathLayer violationRouter owns navigation
No popToRoot on tab re-tapUX expectationHandle tab selection

スコア

総合スコア

60/100

リポジトリの品質指標に基づく評価

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

+10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

レビュー

💬

レビュー機能は近日公開予定です