iOS

NavigationStackを分かりやすく解説

SwiftUIで画面遷移を行う場合、NavigationView + NavigationLinkを利用しますがiOS16からはNavigationViewがdeprecatedとなり、新しくNavigationStackが登場しました。

この記事ではNavigationStackの使い方を分かりやすく解説していきます。

全体像

NavigationStackには3種類の画面遷移の方法が用意されています。

  • ① リンクを押させて直接画面遷移させる
  • ② リンクを押させて、押したリンクの結果を受けて、画面遷移を一箇所でさばかせる
  • ③ 画面の遷移状況を管理する配列を用意する。配列操作の結果を受けて、画面遷移を一箇所でさばかせる

それぞれの方法について以下で詳細を解説していきます。

 

① リンクを押させて直接画面遷移させる

これは今まであったNavigationViewのやり方と同じです。

簡単で分かりやすいので、これで良い気がします。ただ、深い階層から一気に戻る方法は、iOS16以降の環境ではdeprecatedとなっています。

iOS16未満でのNavigationLinkにはフラグを使って遷移を発火させるやり方が提供されていて、それを駆使して、深い階層にBindingで渡したフラグをfalseにさせて一気に戻る方法が一般的でした。

コードはこんな感じになります。
設定画面を想定したトップ画面からメニュー画面に移り、さらにメニュー詳細画面に遷移する流れを想定しています。

設定画面

import SwiftUI

struct StackTest: View {
    var body: some View {
        NavigationStack { // 画面遷移したい箇所をNavigationStackで囲む
            Section("設定画面を想定") {
                List {
                    NavigationLink("メニュー1") {
                        Menu1View()
                    }
                    
                    NavigationLink("メニュー2") {
                        Menu2View()
                    }
                    
                    NavigationLink("メニュー3") {
                        Menu3View()
                    }
                    
                    NavigationLink("メニュー4") {
                        Menu4View()
                    }
                }
            }
        }
    }
}

 

メニュー1画面

遷移先から更に遷移させる場合には、NavigationStackで囲む必要はありません。
戻るボタンは自動で表示されます。

import SwiftUI

struct Menu1View: View {
    var body: some View {
        
        // NavigationStackは不要
        
        VStack {
            Text("Menu1です")
            
            Spacer()
            
            NavigationLink("詳細画面へ") {
                Menu1DetailView()
            }
        }
    }
}

 

メニュー1詳細画面

詳細画面から一気にトップへ戻る方法はdeprecatedとなっているので解説しません。

import SwiftUI

struct Menu1DetailView: View {
    var body: some View {
        Text("Menu1DetailViewです")
    }
}

 

② リンクを押させて、押したリンクの結果を受けて、画面遷移を一箇所でさばかせる

iOS16のNavigationStack + NavigationLinkに追加された機能です。

設定画面

実装の流れは

  • NavigationLinkの第二引数valueに「画面を特定させるキー」をセット。
  • navigationDestinationモディファイアのforパラメータに「画面を特定させるキー」の型を指定。
  • navigationDestinationモディファイアの中で受け取った「画面を特定させるキー」を元に画面遷移処理を記述する。

といった感じです。forに指定するキーはHashableプロトコルに準拠していれば何でもいいようです。

第二階層から第三階層に遷移させる「”詳細画面へ”」という項目がnavigationDestinationモディファイア内のswitch文にあるのに注目です。
遷移先の画面遷移処理もここで一元管理できるという訳です。

List周りがForEachで書けてすっきりしました。
簡単な画面だと前節の方がわかりやすそうですが、項目が増えてくるとこっちが良さそうですね。

import SwiftUI

struct StackTest: View {
    var menuNames = [
        "メニュー1",
        "メニュー2",
        "メニュー3",
        "メニュー4",
    ]
    
    
    var body: some View {
        NavigationStack {
            Section("設定画面を想定") {
                List {
                    ForEach (menuNames.indices, id: \.self) { i in
                        NavigationLink(menuNames[i], value: menuNames[i])
                    }
                }
            }
            .navigationDestination(for: String.self) { menuName in
                switch (menuName) {
                case "詳細画面へ": // 第二階層から第三階層への遷移
                    Menu1DetailView()
                case "メニュー1":
                    Menu1View()
                case "メニュー2":
                    Menu1View()
                case "メニュー3":
                    Menu1View()
                case "メニュー4":
                    Menu1View()
                default:
                    Menu1View()
                }
            }
        }
    }
}

前節の「① リンクを押させて直接画面遷移させる」の方でもメニュー名のセットはForEachで書けますが、遷移先のViewを配列で用意してセットさせるとエラーが起きてビルドできませんでした。(原因は分かりません。誰か教えてください。)

見た目は①と一緒

メニュー1画面

遷移先から更に遷移させる場合には、NavigationStackで囲む必要はありません。
戻るボタンは自動で表示されます。

ここでは詳細画面への遷移処理は書かず、valueにキーを渡すだけになります。
実際の画面遷移処理はルート画面のnavigationDestinationモディファイア内で行います。

import SwiftUI

struct Menu1View: View {
    var body: some View {
        VStack {
            Text("Menu1です")
            
            Spacer()
            
            NavigationLink("詳細画面へ", value: "詳細画面へ")
        }
    }
}

第三階層の処理は①と同じなので割愛します。
①同様、詳細画面から一気にトップへ戻る方法はdeprecatedとなっています。

 

③ 画面の遷移状況を管理する配列を用意する。配列操作の結果を受けて、画面遷移を一箇所でさばかせる

NavigationStackの「Stack」がやっと生きる本命のやり方となります。
NavigationLinkとも決別します。

設定画面

実装の流れは

  • 画面の遷移状況を管理するState配列を用意する
  • NavigationStackにその配列をセットする
  • navigationDestinationモディファイアのforパラメータに、State配列の型を指定。
  • 画面遷移したい場所で、その配列に変数をappendする
  • navigationDestinationモディファイアの中にappendした変数が来るので、受け取ったキーを元に遷移処理を記述する。

といった感じです。

遷移先の画面に遷移状況を管理するState配列を渡しているのがポイントです。
遷移先では、この配列にappendすればさらに違う画面に遷移できます。

そして、ここが大きなポイントというか、これだけのためにNavigationStackが存在する感じですが、配列をremoveLastをすれば1つ前の画面に戻れ、removeAllすれば一気にroot画面に戻ることができます。

逆に言えば、一気に戻る必要がなければ前項のやり方が良い気がします。
僕は②のやり方にしています。

import SwiftUI

struct StackTest: View {
    // 画面遷移を管理する配列
    @State var path: [String] = []
    
    var menuNames = [
        "メニュー1",
        "メニュー2",
        "メニュー3",
        "メニュー4",
    ]
    
    
    var body: some View {
        NavigationStack(path: $path) { // pathを指定
            Section("設定画面を想定") {
                List {
                    ForEach (menuNames.indices, id: \.self) { i in
                        Button(action: {
                            path.append(menuNames[i]) // 遷移先を指定
                        }) {
                            Text(menuNames[i])
                        }
                    }
                }
            }
            .navigationDestination(for: String.self) { menuName in
                switch (menuName) {
                case "詳細画面へ": // 第二階層から第三階層への遷移
                    Menu1DetailView()
                case "メニュー1":
                    Menu1View(path: $path) // 遷移先にpathを渡す
                case "メニュー2":
                    Menu2View(path: $path)
                case "メニュー3":
                    Menu3View(path: $path)
                case "メニュー4":
                    Menu4View(path: $path)
                default:
                    Menu1View(path: $path)
                }
            }
        }
    }
}

NavigationLinkじゃなくButtonを使うので、文字が青くなり、遷移を表す > マークも出ないのが気になります…。

メニュー1画面

遷移先から更に遷移させる場合には、NavigationStackで囲む必要はありません。
戻るボタンは自動で表示されます。

次に表示する詳細画面へpathを渡しているのがポイントです。
結局Binding変数のバケツリレーになっているのが旧来のNavigationLinkを使ったイマイチなやり方を継承していて残念です。

pathに次の画面へのキーを渡せば、root画面のnavigationDestinationモディファイア内で処理されます。

import SwiftUI

struct Menu1View: View {
    @Binding var path: [String]
    
    var body: some View {
        VStack {
            Text("Menu1です")
            
            Spacer()
            
            Button(action: {
                path.append("詳細画面へ") // 遷移先を指定
            }) {
                Text("詳細画面へ")
            }
        }
    }
}

メニュー1詳細画面

夢に見た、一気にルート画面に戻ることができます。

import SwiftUI

struct Menu1DetailView: View {
    @Binding var path: [String]
    var body: some View {
        Text("Menu1DetailViewです")
        
        Spacer()
        
        Button(action: {
            path.removeAll()
        }) {
            Text("一気に戻る")
        }
    }
}