[iOS 16][SwiftUI] NavigationViewからNavigationStackに移行する
はじめに
こんにちは。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を使用)
単純にNavigationView
をNavigationStack
に置き換えます。
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() }