[iOS 16][SwiftUI] NavigationViewからNavigationStackに移行する

本記事ではiOS 16以降で利用できるSwiftUIのコンポーネント「NavigationStack」を扱います。
2022.10.18

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

こんにちは。CX事業本部の平屋です。

本記事では、iOS 16以降で利用できるSwiftUIのコンポーネント「NavigationStack」を扱います。

NavigationView(iOS 16.1で非推奨)」と「NavigationStack(iOS 16〜利用可能)」を比較しながら、その違いを見ていきたいと思います。

「NavigationView」と「NavigationStack」は、どちらもPush/Pop遷移のナビゲーションスタックを管理するためのコンポーネントです。

検証環境

  • macOS Monterey 12.6
  • Xcode Version 14.0.1

静的な画面遷移の場合

固定のデータをリストに表示し、項目タップで詳細画面に遷移するケースを扱います。

移行前 (NavigationViewを使用)

struct OldSingleColumnNavigationView: View {
    let contents = ["A", "B", "C"]

    var body: some View {
        // NavigationViewを使用
        NavigationView {
            List {
                ForEach(contents, id: \.self) { content in
                    NavigationLink(destination: Text(content)) {
                        Text(content)
                    }
                }
            }
            .navigationTitle("Top")
        }
    }
}

移行後 (NavigationStackを使用)

単純にNavigationViewNavigationStackに置き換えます。

struct NewSingleColumnNavigationView: View {
    // ...

    var body: some View {
        // NavigationStackを使用
        NavigationStack {
            // ...
        }
    }
}

// ...

動的な画面遷移の場合

リストの項目タップで、非同期でデータを取得し、取得完了後に詳細画面に遷移するケースを扱います。

移行前 (NavigationViewを使用)

こちらのサンプルコードでは、isActiveフラグを指定できるイニシャライザ(init(destination:isActive:label:)、iOS 16で非推奨)で非表示のNavigationLinkを作成し、遷移のタイミングでフラグをtrueにするという、ちょっと回りくどい実装によって実現してます。

struct OldProgrammaticNavigationView: View {
    @State private var isActive = false
    @State private var content = ""

    var body: some View {
        NavigationView {
            ZStack {
                // 画面に表示されないNavigationLink
                NavigationLink(destination: Text(content), isActive: $isActive) {
                    EmptyView()
                }

                // 画面に表示されるリスト
                List {
                    Button {
                        // 非同期データ取得の模擬実装
                        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                            // データを適用し、フラグをtrueにする
                            content = "received content"
                            isActive = true
                        }
                    } label: {
                        Text("Get Content")
                    }
                }
            }
            .navigationTitle("Top")
        }
    }
}

移行後 (NavigationStackを使用)

移行後の場合、以下の実装で実現できます。移行前と比べると直感的な実装になっているかと思います。

  • (1) 遷移先画面に紐づくデータの入れ物を用意
  • (2) (1)と共にNavigationStackを作成
  • (3) navigationDestinationモディファイアでデータに対する遷移先を指定
  • (4) 遷移させたいタイミングで(1)にデータを追加
struct NewProgrammaticNavigationView: View {
    // (1)
    @State private var path: [String] = []

    var body: some View {
        // (2)
        NavigationStack(path: $path) {
            List {
                Button {
                    // 非同期データ取得の模擬実装
                    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                        // (4)
                        path.append("received content")
                    }
                } label: {
                    Text("Get Content")
                }
            }
            // (3)
            .navigationDestination(for: String.self) { string in
                Text(string)
            }
            .navigationTitle("Top")
        }
    }
}

また、pathに複数のデータを与えてナビゲーションスタックに複数の画面をセットしたり、pathを空にすることによってルートにPop遷移させることもできるようです。

func updatePath() {
    path = ["content1", "content2"]
}

func popToRoot() {
    path.removeAll()
}

参考資料