swiftui-navigation について
Swift 6.3でiOSアプリを開発する人向けのSwiftUIナビゲーションガイドです。スタック、分割レイアウト、シート、タブ、ディープリンクと、ルーティングやレビューの指針を扱います。
主な機能
- 01型付きNavigationStackルート
- 02複数列とカスタム分割レイアウト
- 03シート表示とルーティング
- 04タブごとの独立したナビゲーション
- 05ディープリンクの解析と検証
- 06ナビゲーションのレビュー項目
活用シーン
- 01アプリのタブごとに別のナビゲーションスタックを管理
- 02iPadやMac向けのマスター・詳細画面を構築
- 03選択したモデルの編集シートを表示
- 04リンク検証後にアプリのナビゲーションを更新
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill swiftui-navigationインストール後、Claude Code で/swiftui-navigationを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
evals/evals.jsonreferences/deeplinks.mdnavigationstack.mdsheets.mdtabview.md詳細
SwiftUI ナビゲーション
iOS 26 以降を対象とした SwiftUI アプリのナビゲーションパターンです。Swift 6.3 に対応しています。プッシュナビゲーション、複数列レイアウト、シート表示、タブアーキテクチャ、ディープリンクについて説明します。特に記載がない限り、iOS 17 以降でも後方互換性があります。
目次
- NavigationStack(プッシュナビゲーション)
- NavigationSplitView(複数列)
- シート表示
- タブベースのナビゲーション
- ディープリンク
- よくある間違い
- レビュー用チェックリスト
- 参考資料
NavigationStack(プッシュナビゲーション)
プログラムによるプッシュナビゲーションには、型付きの [Route] バインディングと NavigationStack を使用します。ルートを Hashable enum として定義し、.navigationDestination(for:) で対応付けると、パスがコンパイル時に型チェックされます。1 つのスタックで異なる型のルート値を保持する必要がある場合に限り、NavigationPath を使用します。
enum Route: Hashable {
case item(id: Item.ID)
}
struct ContentView: View {
@State private var path: [Route] = []
let items: [Item]
var body: some View {
NavigationStack(path: $path) {
List(items) { item in
NavigationLink(value: Route.item(id: item.id)) {
ItemRow(item: item)
}
}
.navigationDestination(for: Route.self) { route in
switch route {
case .item(let id):
DetailView(itemID: id)
}
}
.navigationTitle("Items")
}
}
}
プログラムによるナビゲーション:
path.append(.item(id: item.id)) // Push
path.removeLast() // Pop one
path = [] // Pop to root
ルーターパターン: 複雑なナビゲーションを持つアプリでは、パスとシートの状態を管理するルーターオブジェクトを使用します。各タブには、.environment() で注入した独自のルーターインスタンスを割り当てます。単一の .navigationDestination(for:) ブロック、または共有の withAppRouter() モディファイアを使って、遷移先のマッピングを一元管理します。
タブごとのスタック、一元化された遷移先のマッピング、汎用的なタブルーティングなど、ルーターの詳しい例については references/navigationstack.md を参照してください。
NavigationSplitView(複数列)
iPad と Mac のサイドバー・詳細レイアウトには、NavigationSplitView を使用します。iPhone ではスタックナビゲーションに切り替わります。
struct MasterDetailView: View {
@State private var selectedItem: Item?
var body: some View {
NavigationSplitView {
List(items, selection: $selectedItem) { item in
NavigationLink(value: item) { ItemRow(item: item) }
}
.navigationTitle("Items")
} detail: {
if let item = selectedItem {
ItemDetailView(item: item)
} else {
ContentUnavailableView("Select an Item", systemImage: "sidebar.leading")
}
}
}
}
カスタムの分割列(手動の HStack)
選択状態とは独立した専用の通知列など、カスタムの複数列レイアウトには、horizontalSizeClass を確認する手動の HStack 分割を使用します。
@MainActor
struct AppView: View {
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@AppStorage("showSecondaryColumn") private var showSecondaryColumn = true
var body: some View {
HStack(spacing: 0) {
primaryColumn
if shouldShowSecondaryColumn {
Divider().edgesIgnoringSafeArea(.all)
secondaryColumn
}
}
}
private var shouldShowSecondaryColumn: Bool {
horizontalSizeClass == .regular
&& showSecondaryColumn
}
private var primaryColumn: some View {
TabView { /* tabs */ }
}
private var secondaryColumn: some View {
NotificationsTab()
.environment(\.isSecondaryColumn, true)
.frame(maxWidth: .secondaryColumnWidth)
}
}
完全な制御が必要な場合や、標準的ではない第 2 列が必要な場合は、手動の HStack 分割を使用します。カスタマイズを最小限に抑えた標準的なシステムレイアウトを使う場合は、NavigationSplitView を使用します。
シート表示
状態が選択中のモデルを表す場合は、.sheet(isPresented:) より .sheet(item:) を優先してください。シートは自身のアクションを管理し、内部で dismiss() を呼び出すようにします。
@State private var selectedItem: Item?
.sheet(item: $selectedItem) { item in
EditItemSheet(item: item)
}
表示サイズ(iOS 18 以降): .presentationSizing でシートのサイズを制御します。
.sheet(item: $selectedItem) { item in
EditItemSheet(item: item)
.presentationSizing(.form) // .form, .page, .fitted, .automatic
}
PresentationSizing の値:
.automatic-- プラットフォームのデフォルト.page-- およそ用紙サイズ。情報コンテンツ向け.form--.pageより少し狭いサイズ。フォーム形式の UI 向け.fitted-- コンテンツの理想サイズに合わせたサイズ
詳細調整:.fitted(horizontal:vertical:) はサイズ調整する軸を制限します。.sticky(horizontal:vertical:) は、指定した寸法で拡大しますが、縮小しません。
閉じる操作の保護: iOS/iPadOS では、.interactiveDismissDisabled(hasUnsavedChanges) を使用し、シート内に明示的な保存/破棄アクションを用意します。macOS 15 以降では、ウインドウを閉じる際の確認に .dismissalConfirmationDialog("Discard?", shouldPresent: hasUnsavedChanges) を使用します。
プログラムから閉じる操作はすべて、同じ保存/検証/破棄の処理を経由させます。
interactiveDismissDisabled が保護するのは、インタラクティブな閉じる操作のみです。
enum によるシートルーティング: Identifiable に準拠する SheetDestination enum を定義してルーターに保持し、共有のビュー修飾子を使って対応付けます。これにより、子ビューからプロパティを順に渡さずにシートを表示できます。一元化されたシートルーティングの詳しいパターンについては references/sheets.md を参照してください。
タブベースのナビゲーション
拡張性の高いタブアーキテクチャには、選択バインディングと Tab API を使用します。各タブのコンテンツは、独立した NavigationStack でラップします。
struct MainTabView: View {
@State private var selectedTab: AppTab = .home
var body: some View {
TabView(selection: $selectedTab) {
Tab("Home", systemImage: "house", value: .home) {
NavigationStack { HomeView() }
}
Tab("Search", systemImage: "magnifyingglass", value: .search) {
NavigationStack { SearchView() }
}
Tab("Profile", systemImage: "person", value: .profile) {
NavigationStack { ProfileView() }
}
}
}
}
副作用を伴うカスタムバインディング: 選択状態の変更は関数経由で行い、選択状態を変える代わりにアクションを実行する特殊なタブ(例:作成タブ)を検出します。
iOS 26 のタブ追加機能
.searchを指定したTab(value:role:)-- 専用の検索タブとして指定し、システム既定の検索タイトル、アイコン、ピン留め動作を適用します.tabViewSearchActivation(_:)-- 検索タブのアクティベーションとディアクティベーションの動作を制御します.tabBarMinimizeBehavior(_:)--.onScrollDown、.onScrollUp、.never(iPhone のみ).tabViewSidebarHeader/Footer-- iPadOS/macOS でサイドバーのセクションをカスタマイズします.tabViewBottomAccessory { }-- タブバーの下にコンテンツを追加します(例:再生中バー)TabSection--.tabPlacement(.sidebarOnly)を指定して、タブをサイドバーのセクションにグループ化します
カスタムバインディング、動的なタブ、サイドバーのカスタマイズなど、TabView の詳細なパターンについては、references/tabview.md を参照してください。
ディープリンク
解析 → 検証 → 確定の順に処理します。ナビゲーションを変更せずに、型付きルートに解析します。スキーム、ホスト、パス、識別子の形式、認可、遷移先の存在を検証してから、タブとパスをアトミックに更新します。無効なリンクによって、現在のナビゲーションが変更されてはなりません。
ユニバーサルリンク
ユニバーサルリンクを使うと、iOS で標準の HTTPS URL からアプリを開けます。次の設定が必要です。
/.well-known/apple-app-site-associationに Apple App Site Association(AASA)ファイルを配置する- Associated Domains エンタイトルメント(
applinks:example.com)を設定する
SwiftUI でユニバーサルリンクとカスタム URL スキームを処理するには、.onOpenURL を使用します。
@main
struct MyApp: App {
@State private var router = Router()
var body: some Scene {
WindowGroup {
ContentView()
.environment(router)
.onOpenURL { url in router.handle(url: url) }
}
}
}
カスタム URL スキーム
Info.plist の CFBundleURLTypes にスキームを登録します。.onOpenURL で処理します。公開共有するリンクには、カスタムスキームよりユニバーサルリンクを優先してください。ウェブへのフォールバックとドメインの検証が可能です。
Handoff(NSUserActivity)
.userActivity() でアクティビティを宣言し、.onContinueUserActivity() で Handoff やその他のユーザーアクティビティを受け取ります。Info.plist の NSUserActivityTypes にアクティビティタイプを宣言します。isEligibleForHandoff = true を設定し、フォールバックとして webpageURL を指定します。
AASA の設定、ルーターでの URL 処理、カスタム URL スキーム、NSUserActivity の継続処理の詳しい例については、references/deeplinks.md を参照してください。
よくある間違い
- 非推奨の
NavigationViewを使用する —NavigationStackまたはNavigationSplitViewを使用します - すべてのタブで同じナビゲーションパスまたはルーターを共有する — タブごとに独自のパスが必要です
- 状態がモデルを表す場合に
.sheet(isPresented:)を使用する — 代わりに.sheet(item:)を使用します - ナビゲーションパスにビューインスタンスを保存する — 軽量な
Hashableのルートデータを保存します @Observableのルーターオブジェクトを別の@Observableオブジェクトの中にネストする- 古い
.tabItem { }API よりも、TabView(selection:)と組み合わせたTab(value:)を優先します - iPad でも
tabBarMinimizeBehaviorが動作すると想定する — iPhone のみ対応です - 複数の場所でディープリンクを処理する — URL の解析をルーターに集約します
- シートのフレーム寸法をハードコードする — 代わりに
.presentationSizing(.form)を使用します - ルータークラスに
@MainActorを付け忘れる — Swift 6 の並行処理安全性に必要です
レビューチェックリスト
-
NavigationStackを使用している(NavigationViewではない) - 各タブに独立したパスを持つ独自の
NavigationStackがある - ルート enum が安定した識別子を持つ
Hashableである -
.navigationDestination(for:)ですべてのルートタイプをマッピングしている -
.sheet(isPresented:)より.sheet(item:)を優先している - シートが内部で独自の閉じる処理を管理している
- ルーターオブジェクトが
@MainActorと@Observableである - ディープリンク URL をナビゲーションの前に解析し、検証している
- ユニバーサルリンク用の AASA と Associated Domains が設定されている
- タブの選択にバインディングと
Tab(value:)を使用している
参考資料
- NavigationStack とルーターパターン:references/navigationstack.md
- シートの表示とルーティング:references/sheets.md
- TabView のパターンと iOS 26 API:references/tabview.md
- ディープリンク、ユニバーサルリンク、Handoff:references/deeplinks.md
- アーキテクチャと状態管理:
swiftui-patternsスキルを参照してください - レイアウトとコンポーネント:
swiftui-layout-componentsスキルを参照してください