MENU

SwiftUI NavigationStack完全ガイド:iOS 16からの新しいナビゲーション管理

iOS 16で導入されたNavigationStackは、SwiftUIのナビゲーション管理を根本から変える画期的な機能です。この記事では、NavigationStackの基礎から実践的な使い方まで、わかりやすく解説していきます。

目次

NavigationStackとは?

NavigationStackは、従来のNavigationViewに代わる新しいナビゲーション管理の仕組みです。より宣言的で、プログラムから制御しやすく、型安全なナビゲーションを実現します。

従来のNavigationViewとの違い

NavigationView(旧)

  • ネストが複雑になりがち
  • プログラムからの制御が困難
  • Deep Linkの実装が煩雑

NavigationStack(新)

  • シンプルで読みやすい構造
  • スタック全体を外部から操作可能
  • 型安全で保守性が高い

基本的な使い方

まずは、最もシンプルなNavigationStackから見ていきましょう。

import SwiftUI

struct BasicNavigationExample: View {
    var body: some View {
        NavigationStack {
            List {
                NavigationLink("りんご", value: "Apple")
                NavigationLink("バナナ", value: "Banana")
                NavigationLink("さくらんぼ", value: "Cherry")
            }
            .navigationTitle("果物リスト")
            .navigationDestination(for: String.self) { fruit in
                VStack {
                    Image(systemName: "leaf.fill")
                        .font(.system(size: 100))
                        .foregroundColor(.green)
                    Text(fruit)
                        .font(.largeTitle)
                        .padding()
                }
                .navigationTitle(fruit)
            }
        }
    }
}

このコードのポイントは以下の通りです:

  1. NavigationLinkに画面ではなく「値」を渡す
  2. .navigationDestinationで、その値の型に応じた画面を定義
  3. 値と画面の結びつけを分離できる

NavigationPathでスタックを管理する

NavigationStackの真の力は、NavigationPathを使った状態管理にあります。

import SwiftUI

struct PathManagedNavigation: View {
    @State private var path = NavigationPath()
    
    var body: some View {
        NavigationStack(path: $path) {
            VStack(spacing: 20) {
                Text("現在のスタック深度: \(path.count)")
                    .font(.headline)
                
                Button("画面1へ") {
                    path.append("Screen1")
                }
                
                Button("画面2へ直接ジャンプ") {
                    path.append("Screen1")
                    path.append("Screen2")
                }
                
                Button("3階層深くジャンプ") {
                    path.append("Screen1")
                    path.append("Screen2")
                    path.append("Screen3")
                }
                
                if path.count > 0 {
                    Button("ルートに戻る") {
                        path.removeLast(path.count)
                    }
                    .foregroundColor(.red)
                }
            }
            .navigationTitle("ホーム")
            .navigationDestination(for: String.self) { screen in
                ScreenView(screenName: screen, path: $path)
            }
        }
    }
}

struct ScreenView: View {
    let screenName: String
    @Binding var path: NavigationPath
    
    var body: some View {
        VStack(spacing: 20) {
            Text(screenName)
                .font(.largeTitle)
            
            Text("スタック深度: \(path.count)")
                .foregroundColor(.secondary)
            
            Button("次の画面へ") {
                path.append("\(screenName) -> Next")
            }
            
            Button("ルートに戻る") {
                path.removeLast(path.count)
            }
            .foregroundColor(.red)
        }
        .navigationTitle(screenName)
    }
}

このコードでは:

  • プログラムから自由にナビゲーションスタックを操作
  • 任意の階層へ一気にジャンプ
  • ルートへ一発で戻る
  • 現在のスタック状態を監視

といったことが簡単に実現できます。

型安全な配列でパスを管理

より型安全な方法として、具体的な型の配列でパスを管理することもできます。

import SwiftUI

enum Destination: Hashable {
    case profile(User)
    case settings
    case detail(ItemDetail)
}

struct User: Hashable {
    let id: Int
    let name: String
}

struct ItemDetail: Hashable {
    let id: Int
    let title: String
}

struct TypeSafeNavigation: View {
    @State private var path: [Destination] = []
    
    var body: some View {
        NavigationStack(path: $path) {
            List {
                Button("プロフィールを見る") {
                    path.append(.profile(User(id: 1, name: "太郎")))
                }
                
                Button("設定画面へ") {
                    path.append(.settings)
                }
                
                Button("詳細を見る") {
                    path.append(.detail(ItemDetail(id: 1, title: "商品A")))
                }
            }
            .navigationTitle("メニュー")
            .navigationDestination(for: Destination.self) { destination in
                destinationView(for: destination)
            }
        }
    }
    
    @ViewBuilder
    func destinationView(for destination: Destination) -> some View {
        switch destination {
        case .profile(let user):
            VStack {
                Image(systemName: "person.circle.fill")
                    .font(.system(size: 100))
                Text(user.name)
                    .font(.title)
            }
            .navigationTitle("プロフィール")
            
        case .settings:
            Text("設定画面")
                .navigationTitle("設定")
            
        case .detail(let item):
            VStack {
                Text(item.title)
                    .font(.title)
                Text("ID: \(item.id)")
                    .foregroundColor(.secondary)
            }
            .navigationTitle("詳細")
        }
    }
}

列挙型を使うことで:

  • コンパイル時に型チェック
  • 画面遷移のパターンが明確
  • リファクタリングが容易
  • バグを未然に防止

といったメリットが得られます。

実践的な例:Todo アプリ

実際のアプリケーションでの使用例を見てみましょう。

import SwiftUI

struct Todo: Identifiable, Hashable {
    let id = UUID()
    var title: String
    var isCompleted: Bool
}

struct TodoApp: View {
    @State private var todos = [
        Todo(title: "SwiftUIを学ぶ", isCompleted: false),
        Todo(title: "NavigationStackを理解する", isCompleted: false),
        Todo(title: "アプリを作る", isCompleted: false)
    ]
    
    @State private var path: [Todo] = []
    
    var body: some View {
        NavigationStack(path: $path) {
            List {
                ForEach(todos) { todo in
                    NavigationLink(value: todo) {
                        HStack {
                            Image(systemName: todo.isCompleted ? "checkmark.circle.fill" : "circle")
                                .foregroundColor(todo.isCompleted ? .green : .gray)
                            Text(todo.title)
                                .strikethrough(todo.isCompleted)
                        }
                    }
                }
            }
            .navigationTitle("Todoリスト")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button("全て完了") {
                        for index in todos.indices {
                            todos[index].isCompleted = true
                        }
                    }
                }
            }
            .navigationDestination(for: Todo.self) { todo in
                TodoDetailView(todo: todo, path: $path)
            }
        }
    }
}

struct TodoDetailView: View {
    let todo: Todo
    @Binding var path: [Todo]
    
    var body: some View {
        VStack(spacing: 30) {
            Image(systemName: todo.isCompleted ? "checkmark.circle.fill" : "circle")
                .font(.system(size: 100))
                .foregroundColor(todo.isCompleted ? .green : .gray)
            
            Text(todo.title)
                .font(.title)
            
            Text(todo.isCompleted ? "完了済み" : "未完了")
                .font(.headline)
                .foregroundColor(todo.isCompleted ? .green : .orange)
            
            Spacer()
            
            Button("リストに戻る") {
                path.removeAll()
            }
            .buttonStyle(.borderedProminent)
        }
        .padding()
        .navigationTitle("Todo詳細")
    }
}

このTodoアプリの例では:

  • リストから詳細画面への遷移
  • カスタムオブジェクトを値として使用
  • 詳細画面からリストへ直接戻る機能
  • ツールバーでの一括操作

といった実用的な機能を実装しています。

Deep Linkの実装

NavigationStackを使えば、Deep Link(特定の画面に直接遷移)も簡単に実装できます。

import SwiftUI

struct DeepLinkExample: View {
    @State private var path: [Int] = []
    
    var body: some View {
        NavigationStack(path: $path) {
            VStack(spacing: 20) {
                Button("記事1を開く") {
                    openArticle(id: 1)
                }
                
                Button("記事5を開く") {
                    openArticle(id: 5)
                }
                
                Button("記事10の詳細を開く") {
                    openArticleDetail(id: 10)
                }
            }
            .navigationTitle("記事リスト")
            .navigationDestination(for: Int.self) { articleId in
                ArticleView(id: articleId, path: $path)
            }
        }
        .onOpenURL { url in
            handleDeepLink(url)
        }
    }
    
    func openArticle(id: Int) {
        path = [id]
    }
    
    func openArticleDetail(id: Int) {
        path = [id, -id] // -idで詳細画面を表現
    }
    
    func handleDeepLink(_ url: URL) {
        // 例: myapp://article/5
        if let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
           let articleId = Int(components.path.replacingOccurrences(of: "/article/", with: "")) {
            openArticle(id: articleId)
        }
    }
}

struct ArticleView: View {
    let id: Int
    @Binding var path: [Int]
    
    var body: some View {
        VStack {
            Text("記事 \(id)")
                .font(.title)
            
            Button("詳細を見る") {
                path.append(-id)
            }
        }
        .navigationTitle("記事 \(id)")
    }
}

まとめ

NavigationStackは、SwiftUIのナビゲーションをより強力で使いやすくする機能です。

主なメリット:

  • プログラムから完全に制御可能
  • 型安全で保守性が高い
  • Deep Linkの実装が容易
  • コードがシンプルで読みやすい

使い分けのポイント:

  • シンプルなアプリ → NavigationPathで十分
  • 複雑なナビゲーション → 型付き配列を使用
  • Deep Link対応 → pathを外部から設定

iOS 16以降をターゲットにするアプリでは、積極的にNavigationStackを採用することをお勧めします。従来のNavigationViewよりも、はるかに柔軟で強力なナビゲーション管理が実現できます。

プログラミングの独学におすすめ
プログラミング言語の人気オンラインコース
独学でプログラミングを学習している方で、エラーなどが発生して効率よく勉強ができないと悩む方は多いはず。Udemyは、プロの講師が動画で実際のプログラムを動かしながら教えてくれるオンライン講座です。講座の価格は、セール期間中には専門書籍を1冊買うよりも安く済むことが多いです。新しく学びたいプログラミング言語がある方は、ぜひUdemyでオンライン講座を探してみてください。
目次