【SwiftUI】NavigationPathとNavigationStack(path:root:)の使い方!
この記事からわかること
- SwiftUIのNavigationStackの使い方
- init(path:root:)の使用方法
- 孫ビューからルートビューまで一気に戻る方法
- 画面遷移の操作
- NavigationPath構造体とは?
- navigationDestination(for:)とは?
index
[open]
\ アプリをリリースしました /
友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-
posted withアプリーチ
Swift UIのNavigationStackで使用できるinit(path:root:)
とNavigationPath
の使用方法をまとめていきます。
NavigationStackのinit(path:root:)
NavigationStack
構造体にはinit(path:root:)
というイニシャライザが用意されています。このイニシャライザでは引数から変数をバインディングすることで、その変数内に遷移情報をデータとして保持させ、スタック構造のナビゲーション機能をプログラムから管理、操作することが可能になります。
そのため画面遷移時にデータを渡し合うような構造のアプリケーションを開発したい際に役立ちます。
バインディングさせる変数はスタックに依存させたいデータ型(渡す可能性のあるデータ型)の数によって異なります。1種類だけの場合は単純なコレクション型(配列)を複数の種類がある場合はNavigationPath
型を使用します。どちらも同名の型違いとしてイニシャライザが定義されています。
@MainActor init(path: Binding<Data>, @ViewBuilder root: () -> Root) where Data : MutableCollection, Data : RandomAccessCollection, Data : RangeReplaceableCollection, Data.Element : Hashable
@MainActor init(path: Binding<NavigationPath>, @ViewBuilder root: () -> Root) where Data == NavigationPath
また引数root
にはスタックが空の際に表示させるビュー(ルートビューとなる)を渡します。何も渡さなければ設置したNavigationStackを設置したビューになります。
使用するメリット
通常のNavigationStack
では遷移した情報をプログラムから操作することができません。そのため比較的浅いView構造であれば積み上がっているViewを意識することなく使用することができます。
NavigationStack {
NavigationLink {
MyNextView()
} label: {
Label("MyNextViewへ", systemImage: "arrowshape.turn.up.right.fill")
}
}
しかし幾層にも重なるような構造の場合は一気にルートビューへ戻したり、2つ前のページへ戻るなどといった複雑な画面遷移を実装したい機会が増えていきます。その際にinit(path:root:)
を使用することでより細かな画面遷移を実装できるようになります。
init(path:root:)を使用するメリット
- 画面遷移をコレクション形式で管理/操作できる
- 複数の画面を跨いだ遷移が実装可能
- 遷移した履歴が変数に保持され、可視化しやすい
実装例:1つのデータ
まずはシンプルに1つのデータ型に依存した遷移構造を実装してみます。まずNavigationStackにバインディングさせる変数としてシンプルな対象のデータ型の配列を定義します。ここは自身で定義したオリジナルの型でもOKです。
@State private var path: [任意のデータ型] = []
画面遷移にはNavigationLink(_value:)
とnavigationDestination(for:)
メソッドを使用します。NavigationLink
の引数value
に遷移先に渡したい値を指定します。
NavigationLink("表示用テキスト", value: 対象の値)
navigationDestination(for:)
には遷移時に渡すデータ型のタイプを渡し、コールバック関数から該当のデータにアクセスできます。
.navigationDestination(for: データのタイプ) { _ in
// 遷移先にしたいビュー
}
全体のコード
例として「言語名を文字列で保持し、それぞれの画面に遷移させる」コードを実装してみました。
@State private var path: [String] = []
@State private var langs: [String] = ["Swift","Kotlin","Dart"]
var body: some View {
NavigationStack(path: $path) {
List{
ForEach(langs, id:\.self){ lang in
NavigationLink(lang, value: lang)
}
}
.navigationDestination(for: String.self) { text in
ChildView(text: text)
}
}.onChange(of: path) { _ in
print(path)
}
}
onChange
を使って画面遷移時のpath
の中身の変化を出力してみます。シミュレーターを起動してRootView→Swift→戻る→Kotlin→戻る→Dart
のように画面遷移してみると以下のように出力されます。
["Swift"]
[]
["Kotlin"]
[]
["Dart"]
配列の要素を追加して画面遷移させる
NavigationStack
に変数をバインディングしている場合はNavigationLink
を使用しなくても対象の変数に対して要素を追加することでも画面遷移を実装できます。例えば以下のようにappend
を使用して要素を追加することで追加したデータで画面を遷移させることが可能です。
Button {
path.append("Objective-C")
} label: {
Text("path Add Objective-C")
}
配列の要素を削除して画面を戻す
また要素を削除することでも画面を戻すことが可能です。そのためには遷移情報が格納されるpath
を子ビューからも操作できるようにバインディングして渡します。そしてのその変数に対してremoveLast
メソッドを使用して一番最後の要素(最後の画面遷移履歴)を取り除きます。
.navigationDestination(for: String.self) { text in
ChildView(path: $path,text: text) // 変更
}
struct ChildView: View {
@Binding var path:[String]
var text:String
var body: some View {
VStack{
Text(text)
Button {
path.removeLast()
} label: {
Text("path Remove")
}
}
.navigationTitle("ChildView")
}
}
複数の画面を跨いで遷移する
画面遷移を配列操作で実装できるようになったおかげで配列内の要素を複数取り除くことで画面を跨いで遷移させることが可能になります。まずは以下のような数値の増加とともに画面が遷移するコードがあったとします。
struct TestNavigationView: View {
@State private var path: [Int] = []
@State var num:Int = 0
var body: some View {
NavigationStack(path: $path) {
List{
Text("\(num)")
Button {
num += 1
path.append(num)
} label: {
Text("Num Add")
}
}
.navigationDestination(for: Int.self) { num in
ChildView(path: $path,num: $num)
}
}.onChange(of: path) { _ in
print(path)
}
}
}
struct ChildView: View {
@Binding var path:[Int]
@Binding var num:Int
var body: some View {
List{
Text("\(num)")
Button {
num += 1
path.append(num)
} label: {
Text("Num Add")
}
Button {
num -= 1
path.removeLast()
} label: {
Text("Num Sub")
}
Button {
num = 0
path.removeAll()
} label: {
Text("Back RootView")
}
}
.navigationTitle("ChildView")
.navigationBarBackButtonHidden(true)
}
}
そして上記のように画面遷移したとするとpath
の中身は以下のように遷移した分だけ配列の要素が追加されていきます。この配列から後2個を取り除いたり、配列自体を空にすることでルートビューまで一気に戻ることができるようになります。
[1]
[1, 2]
[1, 2, 3] // さらに深くへ進めることもできる
[1, 2, 3, 4]
続いて複数のデータ型を扱う場合を見ていきます。そのためにはNavigationPath
構造体についてもう少し理解しておきます。
NavigationPath構造体とは?
struct NavigationPath
NavigationPath
構造体はスタック構造の遷移情報を管理するためのコレクション形式のデータ型です。コレクション形式なので配列や辞書型などと同じような値の集合になっており、また要素を操作するために必要なappend
メソッドやremoveLast
メソッド、要素数を取得できるcount
プロパティが用意されています。
NavigationPath
は複数のデータ型を扱いたい時に使用します。そのためNavigationPathでは格納されているデータ型を消去することで異なるタイプを保存できるようになっています。
実装例:複数のデータ
では複数のデータを渡す可能性がある場合を実装してみます。まずは同様にNavigationStackにバインディングさせる変数をNavigationPath型で定義します。
@State private var path:NavigationPath = NavigationPath()
続いてNavigationStack
にバインディングさせたら遷移させたいデータ型ごとにnavigationDestination
を呼び出します。例えばColor
構造体とString
構造体を扱う場合は以下のように2つ用意します。
.navigationDestination(for: Color.self) { color in
color.self
}
.navigationDestination(for: String.self) { text in
ChildView(path:$path,text: text)
}
全体のコードは以下のようになりました。プログラム的には非常に意味のないものになっていますが、ご容赦ください。。。
struct TestNavigationView: View {
@State private var path:NavigationPath = NavigationPath()
@State private var langs: [String] = ["Swift","Kotlin","Dart"]
var body: some View {
NavigationStack(path: $path) {
List{
ForEach(langs, id:\.self){ lang in
NavigationLink(lang, value: lang)
}
Button {
path.append(Color.orange)
} label: {
Text("Display Color")
}
}
.navigationDestination(for: Color.self) { color in
color.self
}
.navigationDestination(for: String.self) { text in
ChildView(path:$path,text: text)
}
}.onChange(of: path) { _ in
print("-------")
print(path)
}
}
}
struct ChildView: View {
@Binding var path:NavigationPath
var text:String
var body: some View {
List{
Text(text)
Button {
path.append(Color.cyan)
} label: {
Text("Add Color")
}
Button {
path.removeLast()
} label: {
Text("remove")
}
Button {
path.append("Objective-C")
} label: {
Text("Add Objective")
}
}
.navigationTitle("ChildView")
.navigationBarBackButtonHidden(true)
}
}
上記のように「Swift」→「Objective-C」→「Color.cyan」と変化させた場合はpath
の中身は以下のようになります。
NavigationPath(_items: SwiftUI.NavigationPath.(unknown context at $10a2ef200).Representation.eager([SwiftUI.(unknown context at $10a2eef88).CodableItemBox<Swift.String>]), subsequentItems: [], iterationIndex: 0)
-------
NavigationPath(_items: SwiftUI.NavigationPath.(unknown context at $10a2ef200).Representation.eager([SwiftUI.(unknown context at $10a2eef88).CodableItemBox<Swift.String>, SwiftUI.(unknown context at $10a2eef88).CodableItemBox<Swift.String>]), subsequentItems: [], iterationIndex: 0)
-------
NavigationPath(_items: SwiftUI.NavigationPath.(unknown context at $10a2ef200).Representation.eager([SwiftUI.(unknown context at $10a2eef88).CodableItemBox<Swift.String>, SwiftUI.(unknown context at $10a2eef88).CodableItemBox<Swift.String>, SwiftUI.(unknown context at $10a2eee78).ItemBox<SwiftUI.Color>]), subsequentItems: [], iterationIndex: 0)
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。