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("一気に戻る")
}
}
}