mcpField
A
axiom-swiftui-nav-ref

axiom-swiftui-nav-ref

github.com/charleswiltgen開発1.2k198 回インストール今日更新
リポジトリ

axiom-swiftui-nav-ref について

SwiftUI開発者向けのナビゲーションAPIリファレンスです。NavigationStack、NavigationSplitView、NavigationPath、タブ連携に加え、ディープリンク、状態復元、APIの変遷を解説します。

主な機能

  1. 01NavigationStackとNavigationPathの利用
  2. 02適応型の複数カラムナビゲーション
  3. 03ディープリンクとURLルーティング
  4. 04ナビゲーション状態の保存と復元
  5. 05タブとナビゲーションスタックの連携

活用シーン

  1. 01型安全なプログラムによる画面遷移の実装
  2. 02iPhoneとiPad向けの適応型ナビゲーション設計
  3. 03URLからアプリ内の指定画面を開く
  4. 04アプリ再起動後のナビゲーション状態の復元

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-swiftui-nav-ref

インストール後、Claude Code で/axiom-swiftui-nav-refを入力して読み込まれたか確認します。

詳細

SwiftUI Navigation API リファレンス

概要

SwiftUI のナビゲーション API は、シンプルなスタックから複雑な複数列レイアウトまで対応する、データ駆動型のプログラムによるナビゲーションを提供します。iOS 16(2022 年)で NavigationStack と NavigationSplitView とともに導入され、iOS 18(2024 年)では Tab と Sidebar の統合によって進化し、iOS 26(2025 年)では Liquid Glass デザインによって洗練されました。

進化のタイムライン
  • 2022 年(iOS 16) NavigationStack、NavigationSplitView、NavigationPath、値ベースの NavigationLink
  • 2024 年(iOS 18) Tab と Sidebar の統合、sidebarAdaptable スタイル、ズームナビゲーション遷移
  • 2025 年(iOS 26) Liquid Glass のナビゲーションバー、下部に配置される検索、フローティングタブバー、backgroundExtensionEffect
主な機能
  • データ駆動型ナビゲーション NavigationPath がスタックの状態を表し、プログラムによるプッシュ/ポップとディープリンクを可能にします
  • 複数列レイアウト NavigationSplitView が自動的に適応します(iPad では 3 列 → iPhone では単一スタック)
  • 状態の復元 Codable NavigationPath と SceneStorage により、アプリの起動をまたいで状態を保持します
  • タブとの統合 タブ切り替え時も状態を保持する、タブごとの NavigationStack(iOS 18 以降)
  • Liquid Glass ガラスのナビゲーションバー、サイドバー、ツールバーが自動的に適用されます(iOS 26 以降)
UIKit ではなく SwiftUI を使う場合
  • SwiftUI ナビゲーション 新しいアプリ、マルチプラットフォーム、シンプルなナビゲーションフロー → NavigationStack/SplitView を使用します
  • UINavigationController 複雑なコーディネーターのパターン、レガシーコード、特定の UIKit 機能 → UIKit を検討します
関連スキル
  • アンチパターン、判断ツリー、プレッシャーシナリオには axiom-swiftui-nav を使用します
  • ナビゲーションの問題を体系的にトラブルシューティングするには axiom-swiftui-nav-diag を使用します

このスキルを使う場合

次の場合にこのスキルを使用します:

  • ナビゲーション API の実装 NavigationStack、NavigationSplitView、NavigationPath、Tab+Navigation
  • ディープリンクまたは状態の復元 URL ルーティング、Codable NavigationPath、SceneStorage
  • iOS 26 以降の機能の導入 Liquid Glass ナビゲーション、下部に配置される検索、タブバーの最小化
  • ナビゲーションアーキテクチャの選択 Stack と SplitView の比較、コーディネーターのパターン

API の進化

タイムライン
年iOS バージョン主な機能
2020iOS 14NavigationView(iOS 16 で非推奨)
2022iOS 16NavigationStack、NavigationSplitView、NavigationPath、値ベースの NavigationLink
2024iOS 18Tab と Sidebar の統合、sidebarAdaptable、TabSection、ズーム遷移
2025iOS 26Liquid Glass ナビゲーション、backgroundExtensionEffect、tabBarMinimizeBehavior
NavigationView(非推奨)

NavigationView は iOS 16 以降、非推奨です。新しいコードでは、NavigationStack(単一列のプッシュ/ポップ)または NavigationSplitView(複数列)のみを使用してください。主な改善点は、リンクごとの isActive バインディングを単一の NavigationPath に置き換えたこと、値ベースの型安全性、組み込みの Codable 状態復元です。「Migrating to new navigation types」のドキュメントを参照してください。


NavigationStack 完全リファレンス

NavigationStack は、iPhone の「設定」や macOS の「システム設定」のようなプッシュ/ポップ型のインターフェースを表します。

1.1 NavigationStack の作成
基本的な NavigationStack
NavigationStack {
    List(Category.allCases) { category in
        NavigationLink(category.name, value: category)
    }
    .navigationTitle("Categories")
    .navigationDestination(for: Category.self) { category in
        CategoryDetail(category: category)
    }
}
Path バインディングを使う場合(WWDC 2022、6:05)
struct PushableStack: View {
    @State private var path: [Recipe] = []
    @StateObject private var dataModel = DataModel()

    var body: some View {
        NavigationStack(path: $path) {
            List(Category.allCases) { category in
                Section(category.localizedName) {
                    ForEach(dataModel.recipes(in: category)) { recipe in
                        NavigationLink(recipe.name, value: recipe)
                    }
                }
            }
            .navigationTitle("Categories")
            .navigationDestination(for: Recipe.self) { recipe in
                RecipeDetail(recipe: recipe)
            }
        }
        .environmentObject(dataModel)
    }
}

Path バインディング、値を表示する NavigationLink、navigationDestination(for:) の組み合わせが、データ駆動型ナビゲーションの中核となるパターンです。

1.2 NavigationLink(値ベース)
値を表示する NavigationLink
// Correct: Value-based (iOS 16+)
NavigationLink(recipe.name, value: recipe)

// Correct: With custom label
NavigationLink(value: recipe) {
    RecipeTile(recipe: recipe)
}

// Deprecated: View-based (iOS 13-15)
NavigationLink(recipe.name) {
    RecipeDetail(recipe: recipe)  // Don't use in new code
}
NavigationLink と NavigationStack の連携方法
  1. NavigationStack が path コレクションを管理します
  2. 値を表示するリンクをタップすると、その値が path に追加されます
  3. NavigationStack は、path の値に対して navigationDestination 修飾子を対応付けます
  4. 移動先の対応付けに基づいて、ビューがスタックにプッシュされます
1.3 navigationDestination 修飾子
単一の型
.navigationDestination(for: Recipe.self) { recipe in
    RecipeDetail(recipe: recipe)
}
複数の型
NavigationStack(path: $path) {
    RootView()
        .navigationDestination(for: Recipe.self) { recipe in
            RecipeDetail(recipe: recipe)
        }
        .navigationDestination(for: Category.self) { category in
            CategoryList(category: category)
        }
        .navigationDestination(for: Chef.self) { chef in
            ChefProfile(chef: chef)
        }
}
ナビゲーションのアンチパターン
  • 同じ NavigationStack 階層内で navigationDestination(for:) と NavigationLink(destination:) を決して混在させないでください — 未定義の動作が発生します
  • データ型ごとに navigationDestination(for:) を一度だけ登録してください — 重複すると誤ったビューが表示されます
配置ルール
  • navigationDestination は遅延コンテナの外側に配置します(ForEach の内側には配置しません)
  • コードを整理しやすいよう、関連する NavigationLink の近くに配置します
  • NavigationStack 階層の内側に配置する必要があります
// Correct: Outside lazy container
ScrollView {
    LazyVGrid(columns: columns) {
        ForEach(recipes) { recipe in
            NavigationLink(value: recipe) {
                RecipeTile(recipe: recipe)
            }
        }
    }
}
.navigationDestination(for: Recipe.self) { recipe in
    RecipeDetail(recipe: recipe)
}

// Wrong: Inside ForEach (may not be loaded)
ForEach(recipes) { recipe in
    NavigationLink(value: recipe) { RecipeTile(recipe: recipe) }
        .navigationDestination(for: Recipe.self) { r in  // Don't do this
            RecipeDetail(recipe: r)
        }
}
1.4 NavigationPath

NavigationPath は、異種型のナビゲーションスタックに使用する型消去されたコレクションです。

型付き配列と NavigationPath の比較
// Typed array: All values same type
@State private var path: [Recipe] = []

// NavigationPath: Mixed types
@State private var path = NavigationPath()
NavigationPath の操作
// Append value
path.append(recipe)

// Pop to previous
path.removeLast()

// Pop to root
path.removeLast(path.count)
// or
path = NavigationPath()

// Check count
if path.count > 0 { ... }

// Deep link: Set multiple values
path.append(category)
path.append(recipe)
Codable のサポート
// NavigationPath is Codable when all values are Codable
@State private var path = NavigationPath()

// Encode
let data = try JSONEncoder().encode(path.codable)

// Decode
let codableRep = try JSONDecoder().decode(NavigationPath.CodableRepresentation.self, from: data)
path = NavigationPath(codableRep)

NavigationSplitView 完全リファレンス

NavigationSplitView は、デバイスのサイズに応じて適応する複数列のレイアウトを作成します。

2.1 2 列レイアウト
基本的な 2 列レイアウト(WWDC 2022、10:40)
struct MultipleColumns: View {
    @State private var selectedCategory: Category?
    @State private var selectedRecipe: Recipe?
    @StateObject private var dataModel = DataModel()

    var body: some View {
        NavigationSplitView {
            List(Category.allCases, selection: $selectedCategory) { category in
                NavigationLink(category.localizedName, value: category)
            }
            .navigationTitle("Categories")
        } detail: {
            if let recipe = selectedRecipe {
                RecipeDetail(recipe: recipe)
            } else {
                Text("Select a recipe")
            }
        }
    }
}
2.2 3 列レイアウト

サイドバーと詳細表示の間に content: クロージャーを追加して、中間列を作成します。

NavigationSplitView {
    List(Category.allCases, selection: $selectedCategory) { category in
        NavigationLink(category.localizedName, value: category)
    }
} content: {
    List(dataModel.recipes(in: selectedCategory), selection: $selectedRecipe) { recipe in
        NavigationLink(recipe.name, value: recipe)
    }
} detail: {
    RecipeDetail(recipe: selectedRecipe)
}
2.3 NavigationStack を使用した NavigationSplitView

詳細表示列の内側に NavigationStack(path:) を配置すると、サイドバーの選択状態を維持しながら、グリッドから詳細表示へドリルダウンできます。

NavigationSplitView {
    List(Category.allCases, selection: $selectedCategory) { ... }
} detail: {
    NavigationStack(path: $path) {
        RecipeGrid(category: selectedCategory)
            .navigationDestination(for: Recipe.self) { recipe in
                RecipeDetail(recipe: recipe)
            }
    }
}
2.4 列の表示状態
@State private var columnVisibility: NavigationSplitViewVisibility = .all

NavigationSplitView(columnVisibility: $columnVisibility) {
    Sidebar()
} content: {
    Content()
} detail: {
    Detail()
}

// Programmatically control visibility
columnVisibility = .detailOnly  // Hide sidebar and content
columnVisibility = .all          // Show all columns
columnVisibility = .automatic    // System decides
2.5 自動適応

NavigationSplitView は次のように自動で適応します。

  • iPad の横向き すべての列が表示されます(構成によって異なります)
  • iPad の縦向き/Slide Over オーバーレイまたは単一列に折りたたまれます
  • iPhone 単一のナビゲーションスタック
  • Apple Watch/TV 単一のナビゲーションスタック

選択状態の変更は、iPhone では自動的に push/pop に変換されます。

2.6 iOS 26 以降の Liquid Glass サイドバー(WWDC 2025、323)
NavigationSplitView {
    List { ... }
} detail: {
    DetailView()
}
// Sidebar automatically gets Liquid Glass appearance on iPad/macOS

// Extend content behind glass sidebar
.backgroundExtensionEffect()  // Mirrors and blurs content outside safe area

ディープリンクと URL ルーティング

3.1 ディープリンクのパターン

.onOpenURL で URL を受け取り、URLComponents で解析してから、NavigationPath を操作します。

.onOpenURL { url in
    guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
          let host = components.host else { return }
    path.removeLast(path.count)  // Pop to root first
    // Parse host/path to determine destination, then path.append(value)
}

複数ステップのディープリンク(myapp://category/desserts/recipe/apple-pie)では、URL のパスコンポーネントを順に処理し、解決した値をそれぞれ追加して、ナビゲーションスタック全体を構築します。

包括的なディープリンクの例、エラーの診断、テストのワークフローについては、axiom-swiftui-nav-diag(パターン 3)を参照してください。


状態の復元

4.1 状態の完全な復元(WWDC 2022、18:12)
struct UseSceneStorage: View {
    @StateObject private var navModel = NavigationModel()
    @SceneStorage("navigation") private var data: Data?
    @StateObject private var dataModel = DataModel()

    var body: some View {
        NavigationSplitView {
            List(Category.allCases, selection: $navModel.selectedCategory) { category in
                NavigationLink(category.localizedName, value: category)
            }
            .navigationTitle("Categories")
        } detail: {
            NavigationStack(path: $navModel.recipePath) {
                RecipeGrid(category: navModel.selectedCategory)
            }
        }
        .task {
            // Restore on appear
            if let data = data {
                navModel.jsonData = data
            }
            // Save on changes
            for await _ in navModel.objectWillChangeSequence {
                data = navModel.jsonData
            }
        }
        .environmentObject(dataModel)
    }
}
4.2 Codable 対応の NavigationModel
class NavigationModel: ObservableObject, Codable {
    @Published var selectedCategory: Category?
    @Published var recipePath: [Recipe] = []

    enum CodingKeys: String, CodingKey {
        case selectedCategory
        case recipePathIds  // Store IDs, not full objects
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encodeIfPresent(selectedCategory, forKey: .selectedCategory)
        try container.encode(recipePath.map(\.id), forKey: .recipePathIds)
    }

    init() {}

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.selectedCategory = try container.decodeIfPresent(Category.self, forKey: .selectedCategory)
        let recipePathIds = try container.decode([Recipe.ID].self, forKey: .recipePathIds)
        self.recipePath = recipePathIds.compactMap { DataModel.shared[$0] } // Discard deleted items
    }
}

ID(モデルオブジェクト全体ではなく)を保存し、compactMap を使って削除された項目を安全に処理します。4.1 に示すように、SceneStorage との統合に jsonData 計算プロパティと objectWillChangeSequence を追加します。


タブとナビゲーションの統合

5.1 タブの構文(iOS 18 以降)(WWDC 2024、4:27)
TabView {
    Tab("Watch Now", systemImage: "play") {
        WatchNowView()
    }
    Tab("Library", systemImage: "books.vertical") {
        LibraryView()
    }
    Tab(role: .search) {
        NavigationStack {
            SearchView()
                .navigationTitle("Search")
        }
        .searchable(text: $searchText)
    }
}

検索タブの要件:検索ロールのタブのコンテンツは NavigationStack でラップし、そのスタックに .searchable() を適用する必要があります。NavigationStack がないと、検索フィールドは表示されません。基本的な .searchable のパターン(候補、スコープ、トークン、プログラムによる制御)については、axiom-swiftui-search-ref を参照してください。

5.2 タブごとに NavigationStack を持つ TabView
TabView {
    Tab("Home", systemImage: "house") {
        NavigationStack {
            HomeView()
                .navigationDestination(for: Item.self) { item in
                    ItemDetail(item: item)
                }
        }
    }
    Tab("Settings", systemImage: "gear") {
        NavigationStack {
            SettingsView()
        }
    }
}

各タブに独自の NavigationStack があり、タブを切り替えてもナビゲーション状態が保持されます。

5.3 サイドバーに適応する TabView(WWDC 2024、6:41)
TabView {
    Tab("Watch Now", systemImage: "play") {
        WatchNowView()
    }
    Tab("Library", systemImage: "books.vertical") {
        LibraryView()
    }
    TabSection("Collections") {
        Tab("Cinematic Shots", systemImage: "list.and.film") {
            CinematicShotsView()
        }
        Tab("Forest Life", systemImage: "list.and.film") {
            ForestLifeView()
        }
    }
    TabSection("Animations") {
        // More tabs...
    }
    Tab(role: .search) {
        SearchView()
    }
}
.tabViewStyle(.sidebarAdaptable)

TabSection はサイドバーのグループを作成します。.sidebarAdaptable を有効にすると、iPad ではサイドバー、iPhone ではタブバーになります。.search ロールの検索タブは特別な位置に配置されます。

5.4 タブのカスタマイズ(WWDC 2024、10:45)
@AppStorage("MyTabViewCustomization")
private var customization: TabViewCustomization

TabView {
    Tab("Watch Now", systemImage: "play", value: .watchNow) {
        WatchNowView()
    }
    .customizationID("Tab.watchNow")
    .customizationBehavior(.disabled, for: .sidebar, .tabBar)  // Can't be hidden

    Tab("Optional Tab", systemImage: "star", value: .optional) {
        OptionalView()
    }
    .customizationID("Tab.optional")
    .defaultVisibility(.hidden, for: .tabBar)  // Hidden by default
}
.tabViewCustomization($customization)
5.5 タブのコンテキストメニュー

Tab に .contextMenu(menuItems:) を使用すると、サイドバー上のタブ表示にコンテキストメニューを追加できます(例:Mac での右クリック、iPad のサイドバーでの長押し)。

Tab("Currently Reading", systemImage: "book") {
    CurrentBooksList()
}
.contextMenu {
    Button {
        pinnedTabs.insert(.reading)
    } label: {
        Label("Pin", systemImage: "pin")
    }
    Button {
        showShareSheet = true
    } label: {
        Label("Share", systemImage: "square.and.arrow.up")
    }
}

条件に応じてコンテキストメニューを無効にするには、空のクロージャーを返します。

.contextMenu {
    if canPin {
        Button("Pin", systemImage: "pin") { pin() }
    }
}
iPhoneのタブバーの長押し

Tab の .contextMenu が適用されるのは、サイドバーでの表示のみです(iPad/Mac)。iPhoneのタブバーにコンテキストメニューを追加するには、UIKitとの連携が必要です(Introspectを使って UITabBar に UILongPressGestureRecognizer を追加するか、UITabBarController のサブクラスを使用します)。回避策のパターンについては axiom-swiftui-nav-diag を参照してください。

注意点:非公開の UITabBarButton サブビューに依存しているため、iOSのバージョン間で動作が不安定になる可能性があり、公開APIとして保証されているものではありません。

5.6 プログラムによるタブの表示制御

.hidden(_:) を使うと、アプリの状態に応じてタブを表示/非表示にしながら、ナビゲーション状態を保持できます。

状態に応じたタブの表示制御
Tab("Libraries", systemImage: "square.stack") { LibrariesView() }
    .hidden(context == .browse)  // Hide based on app state

各タブに .hidden(condition) を適用します。この方法で非表示にしたタブはナビゲーション状態を保持します(条件付きの if による描画では、タブが破棄されて再作成されます)。

状態の保持

主な違い:.hidden(_:) はタブの状態を保持しますが、条件付きの描画では保持されません。

// ✅ State preserved when hidden
Tab("Settings", systemImage: "gear") {
    SettingsView()  // Navigation stack preserved
}
.hidden(!showSettings)

// ❌ State lost when condition changes
if showSettings {
    Tab("Settings", systemImage: "gear") {
        SettingsView()  // Navigation stack recreated
    }
}
よくあるパターン
Tab("Beta Features", systemImage: "flask") { BetaView() }
    .hidden(!UserDefaults.standard.bool(forKey: "enableBetaFeatures"))

認証状態、購入状況、デバッグビルドにも同じパターンを適用できます。任意のブール条件を .hidden() にバインドしてください。

アニメーション付きの遷移

状態の変更を withAnimation で囲むと、タブバーのレイアウトが滑らかに切り替わります。

Button("Switch to Browse") {
    withAnimation {
        context = .browse
        selection = .tracks  // Switch to first visible tab
    }
}
// Tab bar animates as tabs appear/disappear
// Uses system motion curves automatically
5.7 iOS 26以降のタブ機能(WWDC 2025、256)
// Tab bar minimization on scroll
TabView { ... }
    .tabBarMinimizeBehavior(.onScrollDown)

// Bottom accessory view (always visible)
TabView { ... }
    .tabViewBottomAccessory {
        PlaybackControls()
    }

// Dynamic visibility (recommended for mini-players)
// ⚠️ Requires iOS 26.1+ (not 26.0)
TabView { ... }
    .tabViewBottomAccessory(isEnabled: showMiniPlayer) {
        MiniPlayerView()
            .transition(.opacity)
    }
// isEnabled: true = shows accessory
// isEnabled: false = hides AND removes reserved space

// Search tab with dedicated search field
Tab(role: .search) {
    NavigationStack {
        SearchView()
            .navigationTitle("Search")
    }
    .searchable(text: $searchText)
}
// Morphs into search field when selected
// ⚠️ NavigationStack wrapper required for search field to appear
// Fallback: If no tab has .search role, the tab view applies search
// to ALL tabs, resetting search state when the selected tab changes
動的な下部アクセサリ

activeTab に応じてアクセサリの内容を切り替え、タブごとに異なる内容を表示できます。ただし、Appleの使用例(Musicのミニプレーヤー)では、全タブ共通の内容が表示されています。@Environment(\.tabViewBottomAccessoryPlacement) を読み取ることで、レイアウトを調整できます。タブバーの上にある場合は .bar(すべてのコントロールを表示)、タブバーが折りたたまれてインライン表示される場合は、それ以外の値(コンパクト表示)になります。

tabViewBottomAccessory は、タブをまたいで共通する内容(再生、ステータスなど)に使用してください。タブ固有の操作には、タブのコンテンツビュー内にフローティングのグラスボタンを配置することをおすすめします。

5.8 タブAPIクイックリファレンス
修飾子対象iOS用途
Tab(_:systemImage:value:content:)—18+選択値を使った新しいタブ構文
Tab(role: .search)—18+モーフィング動作を備えた、検索用のセマンティックタブ
TabSection(_:content:)—18+サイドバービューでタブをグループ化
.contextMenu(menuItems:)Tab18+タブのサイドバー表示にコンテキストメニューを追加
.customizationID(_:)Tab18+ユーザーによるカスタマイズを有効化
.customizationBehavior(_:for:)Tab18+非表示/並べ替えの権限を制御
.defaultVisibility(_:for:)Tab18+初期の表示状態を設定
.hidden(_:)Tab18+状態を保持したプログラムによる表示制御
.tabViewStyle(.sidebarAdaptable)TabView18+iPadではサイドバー、iPhoneではタブを表示
.tabViewCustomization($binding)TabView18+ユーザーによるタブの配置を保持
.tabBarMinimizeBehavior(_:)TabView26+スクロール時に自動で非表示
.tabViewBottomAccessory(isEnabled:content:)TabView26.1+タブバーの下に動的なコンテンツを表示

iOS 26以降のナビゲーション機能

6.1 Liquid Glassナビゲーション(WWDC 2025、323)

Xcode 26でビルドすると、自動的に適用されます。

  • ナビゲーションバーがLiquid Glassになります
  • サイドバーがグラス効果とともにコンテンツの上に浮かびます
  • タブバーが新しいコンパクトな外観で浮かびます
  • ツールバーが自動的にグループ化されます
6.2 背景拡張効果
NavigationSplitView {
    Sidebar()
} detail: {
    HeroImage()
        .backgroundExtensionEffect()  // Content extends behind sidebar
}
6.3 下部に配置される検索(WWDC 2025、256)

検索の基盤となるAPI:.searchable、isSearching、候補、スコープ、トークン、プログラムによる制御については、axiom-swiftui-search-ref を参照してください。このセクションでは、iOS 26で下部に配置されるようになった検索機能の変更点のみを説明します。

NavigationSplitView {
    Sidebar()
} detail: {
    DetailView()
}
.searchable(text: $query, prompt: "What are you looking for?")
// Automatically bottom-aligned on iPhone, top-trailing on iPad
6.4 スクロールエッジ効果
// Automatic blur effect when content scrolls under toolbar
// Remove any custom darkening backgrounds - they interfere

// For dense UIs, adjust sharpness
ScrollView { ... }
    .scrollEdgeEffectStyle(.soft)  // .sharp, .soft
6.5 ズームトランジションを使ったシートの表示

iOS 26 では、シートを表示元のボタンから直接モーフィングできます。表示元のツールバー項目をナビゲーションのズームトランジションのソースにし、シートのコンテンツを遷移先として指定します。

@Namespace private var namespace

// Sheet morphs out of presenting button
.toolbar {
    ToolbarItem {
        Button("Settings") { showSettings = true }
            .matchedTransitionSource(id: "settings", in: namespace)
    }
}
.sheet(isPresented: $showSettings) {
    SettingsView()
        .navigationTransition(.zoom(sourceID: "settings", in: namespace))
}

メニュー、アラート、ポップオーバーなど、ほかのプレゼンテーションも Liquid Glass コントロールからスムーズに表示されます。ダイアログは追加のコードなしで、表示元のボタンから自動的にモーフィングします。

監査のヒント:シートにカスタム背景を適用するために presentationBackground を使用している場合は、削除して新しい Liquid Glass のシート素材を活かすことを検討してください。高さが部分的なシートには、デフォルトでガラスの背景がインセットされるようになりました。

6.6 ツールバーのモーフィングトランジション

iOS 26 では、遷移先の各ビューで独自の .toolbar {} を宣言している場合、NavigationStack の push/pop 中にツールバーが自動的にモーフィングします。一致する toolbar(id:) と ToolbarItem(id:) の ID を持つ項目は、トランジション中も安定して表示され(跳ねることはありません)、一致しない項目はアニメーションしながら表示または非表示になります。

重要なルール:.toolbar {} は NavigationStack 自体ではなく、NavigationStack 内の個々のビューに付けてください。そうしないと、モーフィングの対象がありません。

DefaultToolbarItem、toolbar(id:) による安定した項目、ToolbarSpacer のパターン、トラブルシューティングなど、ツールバーのモーフィング API の全容については、axiom-swiftui-26-ref スキルを参照してください。


ルーター/コーディネーターパターン

7.1 コーディネーターを使う場面

コーディネーターを使う場合:

  • 条件分岐を含む複雑なナビゲーションフローがある
  • ナビゲーションを単独でテストする
  • 複数の画面でナビゲーションロジックを共有する
  • ナビゲーション要件が多い UIKit との連携がある

組み込みのナビゲーションを使う場合:

  • シンプルな直線型または階層型のナビゲーションである
  • 状態の復元が主な関心事である
  • ナビゲーション先が 5~10 個未満である
  • ナビゲーションのユニットテストが不要である
7.2 シンプルなルーターパターン
// Route enum defines all possible destinations
enum AppRoute: Hashable {
    case home
    case category(Category)
    case recipe(Recipe)
    case settings
}

// Router class manages navigation
@Observable
class Router {
    var path = NavigationPath()

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

    func popToRoot() {
        path.removeLast(path.count)
    }

    func pop() {
        if !path.isEmpty {
            path.removeLast()
        }
    }
}

// Usage in views
struct ContentView: View {
    @State private var router = Router()

    var body: some View {
        NavigationStack(path: $router.path) {
            HomeView()
                .navigationDestination(for: AppRoute.self) { route in
                    switch route {
                    case .home:
                        HomeView()
                    case .category(let category):
                        CategoryView(category: category)
                    case .recipe(let recipe):
                        RecipeDetail(recipe: recipe)
                    case .settings:
                        SettingsView()
                    }
                }
        }
        .environment(router)
    }
}

// In child views
struct RecipeCard: View {
    let recipe: Recipe
    @Environment(Router.self) private var router

    var body: some View {
        Button(recipe.name) {
            router.navigate(to: .recipe(recipe))
        }
    }
}
7.3 プロトコルを使ったコーディネーターパターン

大規模なアプリでは、associatedtype Route: Hashable と var path: NavigationPath を持つ Coordinator プロトコルを切り出します。各機能領域で独自のドメイン固有ルートと便利なメソッド(たとえば、パスをリセットしてから遷移する showRecipeOfTheDay())を使い、個別にコーディネーターへ準拠させます。

7.4 ナビゲーションのテスト
// Router is easily testable
func testNavigateToRecipe() {
    let router = Router()
    let recipe = Recipe(name: "Apple Pie")

    router.navigate(to: .recipe(recipe))

    XCTAssertEqual(router.path.count, 1)
}

func testPopToRoot() {
    let router = Router()
    router.navigate(to: .category(.desserts))
    router.navigate(to: .recipe(Recipe(name: "Apple Pie")))

    router.popToRoot()

    XCTAssertTrue(router.path.isEmpty)
}

テストチェックリスト

  • コールドスタート時とアプリの実行中の両方で、ディープリンクが正しく遷移する
  • ルートに戻ると、スタック全体がクリアされる
  • アプリの再起動時に状態が復元される(SceneStorage のキーはシーンごとに一意)
  • 復元時に削除済みの項目が適切に処理される(compactMap)
  • NavigationSplitView が iPhone で正しく折りたたまれる(選択すると遷移する)
  • iOS 26 以降:Liquid Glass の外観、下部に配置された検索、タブバーの最小化

API クイックリファレンス

NavigationStack
NavigationStack { content }
NavigationStack(path: $path) { content }
NavigationSplitView
NavigationSplitView { sidebar } detail: { detail }
NavigationSplitView { sidebar } content: { content } detail: { detail }
NavigationSplitView(columnVisibility: $visibility) { ... }
NavigationLink
NavigationLink(title, value: value)
NavigationLink(value: value) { label }
NavigationPath
path.append(value)
path.removeLast()
path.removeLast(path.count)
path.count
path.codable  // For encoding
NavigationPath(codableRepresentation)  // For decoding
モディファイア
.navigationTitle("Title")
.navigationDestination(for: Type.self) { value in View }
.searchable(text: $query)
.tabViewStyle(.sidebarAdaptable)
.tabBarMinimizeBehavior(.onScrollDown)
.backgroundExtensionEffect()

リソース

WWDC:2022-10054、2024-10147、2025-256、2025-323(新しいデザインで SwiftUI アプリを構築する)

ドキュメント:/swiftui/tabrole/search、/swiftui/view/tabbarminimizebehavior(_:), /swiftui/view/tabviewbottomaccessory(isenabled:content:)

スキル:axiom-swiftui-nav、axiom-swiftui-nav-diag、axiom-swiftui-26-ref、axiom-liquid-glass、axiom-swiftui-search-ref


最終更新 WWDC 2022-10054、WWDC 2024-10147、WWDC 2025-256、WWDC 2025-323(新しいデザインで SwiftUI アプリを構築する)に基づく
プラットフォーム iOS 16 以降、iPadOS 16 以降、macOS 13 以降、watchOS 9 以降、tvOS 16 以降

コメント