NavigationStack
従来のNavigationView
はiOS16以降で非推奨となり、代わりにNavigationStack
の導入が推奨されています。
基本的な使い方
NavigationView
と同じように使うことができます。
struct ContentView: View {
var body: some View {
NavigationStack {
NavigationLink("次の画面へ") {
NextView()
}
}
}
}
NavigationPath
とnavigationDestination
を使った実装
NavigationPath
を与えることで、pathの変更を監視して画面遷移させることができます。
["path A", "path B", "path C"]
と配列の要素数に応じて階層は深くなっていき、最後尾の要素が現在の画面に渡されます。
struct ContentView: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
Button("Pathを追加") {
path.append("new path")
}
.navigationDestination(for: String.self, destination: { _ in
NextView()
})
}
}
}
画面一覧を列挙して自動遷移させる
実はHashable
に準拠するコレクションであれば、何でもNavigationPath
として使うことができます。
これを利用してNavigationStack
の配下に置く画面一覧をenum
で宣言し、その配列をpathとして渡します。
さらに@ViewBuilder
関数を使うことによって、対応するView
まで一挙に宣言してしまいます。
ルートのnavigationDestination
にscreenKey.destination()
を記述しておけば、あとはどこでもpathの配列を操作するだけで、自動的に対応するViewへ遷移させることができます。
enum ScreenKey {
case home
case fruitList
case fruitDetail
// 対応するViewのインスタンスを返す
@ViewBuilder
func destination() -> some View {
switch self {
case .home:
HomeView()
case .fruitList:
FruitListView()
case .fruitDetail:
FruitDetailView()
}
}
}
struct RootView: View {
@State private var path = [ScreenKey]()
var body: some View {
NavigationStack(path: $path) {
HomeView()
// ルートに記述するだけでokです
.navigationDestination(for: ScreenKey.self, destination: { screenKey in
screenKey.destination()
})
}
}
}
@EnvironmentObject
として各画面でpathを操作する
どの階層でもpathを操作できるようにするためにNavigationStack
のenvironmentObject
に設定してしまいます。
これでNavigationStack
配下全てのViewからpathを操作できるようになりました。
以下の実装は
- ホーム画面
- フルーツ一覧画面
- フルーツ詳細画面
と深い階層へ画面が遷移していく実用的なケースです。
詳細画面にはid
が必要なためScreenKey
に値を持たせます。
// environmentObjectに設定するためObservableObjectに準拠させます
class NavigationRouter: ObservableObject {
@Published var path = [ScreenKey]()
enum ScreenKey: Hashable {
case home
case fruitList
case fruitDetail(id: String)
@ViewBuilder
func destination() -> some View {
switch self {
case .home:
HomeView()
case .fruitList:
FruitListView()
case .fruitDetail(let id):
FruitDetailView(id: id)
}
}
}
}
struct RootView: View {
@StateObject private var navigationRouter = NavigationRouter()
var body: some View {
NavigationStack(path: $navigationRouter.path) {
HomeView()
.navigationDestination(for: NavigationRouter.ScreenKey.self, destination: { screenKey in
screenKey.destination()
})
}
// NavigationStack自身のmodifierとして記述するよう注意してください
.environmentObject(navigationRouter)
}
}
struct HomeView: View {
@EnvironmentObject private var navigationRouter: NavigationRouter
var body: some View {
Button("フルーツ一覧画面へ") {
navigationRouter.path.append(.fruitList)
}
}
}
struct FruitListView: View {
@EnvironmentObject private var navigationRouter: NavigationRouter
private let fruitList = [Fruit(id: "0", name: "apple"),
Fruit(id: "1", name: "orange"),
Fruit(id: "2", name: "peach")]
var body: some View {
List(fruitList) { fruit in
Button("\(fruit.name)詳細画面へ") {
navigationRouter.path.append(.fruitDetail(id: fruit.id))
}
}
}
}
struct Fruit {
let id: String
let name: String
}
まとめ
ルートのnavigationDestination
さえ設定してしまえば、以降の階層では配列を操作するだけで、自動的に画面遷移をさせることができました。
NavigationStack
が表示し得る画面はNavigationRouter
に明示的に宣言するといった実装で、まさにSwiftUIって感じが大満足です。
NavigationLink
からの解放も嬉しい。
この実装方法では、ディープリンクや、ログイン状態による分岐、ログアウト処理などを直感的でシンプルに書くこともできるので別で記事にしたいと思います。
お役に立てれば幸いです!
追記
この実装方法では、ディープリンクや、ログイン状態による分岐、ログアウト処理などを直感的でシンプルに書くこともできるので別で記事にしたいと思います。
してみました!