前回のおさらい
Hashable
に準拠するコレクションとして画面一覧をenum
で宣言してNavigationStack
にpathとして渡し、@ViewBuilder
関数を使うことで対応するViewに自動遷移させることができました。
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
}
ログアウト処理など、ルート画面へのpopを直感的に書く
ログイン状態によってログイン画面orホーム画面を表示する、などイニシャル画面の分岐がある一方、ログアウト処理では共にログイン画面に戻る、といった遷移要件はよくあるケースだと思います。
以下の実装では、pathとnavigationBarBackButtonHidden()
を活用することによって、イニシャル画面を分岐させつつ、内部的な配列はどのケースでもログイン画面を経由している状態を実現しています。
これによってルートへのpopを簡潔に記述でき、さらにNavigationStack
標準のpopアニメーションを担保しています。
struct RootView: View {
@StateObject private var navigationRouter = NavigationRouter()
var body: some View {
NavigationStack(path: $navigationRouter.path) {
LoginView()
.navigationDestination(for: NavigationRouter.ScreenKey.self, destination: { screenKey in
screenKey.destination()
})
}
.environmentObject(navigationRouter)
+ .onAppear {
+ setInitialPath()
+ }
}
+ private func setInitialPath() {
+ guard isLoggedIn else { return }
+ navigationRouter.path.append(.home)
+ }
}
struct HomeView: View {
var body: some View {
Text("ホーム画面")
+ .navigationBarBackButtonHidden()
}
}
struct LogoutView: View {
@EnvironmentObject private var navigationRouter: NavigationRouter
+ private func logout() {
+ navigationRouter.path.removeAll()
+ }
}
ディープリンクでも階層構造を担保できる
さらにこの実装では、ディープリンクなどの操作を伴わない画面遷移でも、通常操作ケースと同じように画面階層を担保することができます。
例えば、通常操作ではホーム画面→リスト画面→詳細画面と遷移するのに、詳細画面へディープリンクを使って遷移したことで、ホーム画面→詳細画面の階層構造となってしまい、戻り先が通常操作と異なってしまう、といったケースはよくあると思います。
以下の実装では、配列に.fruitList
を追加することによって、ディープリンクでも画面の階層構造を担保しています。
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()
})
}
+ .onOpenURL(perform: { url in
+ // ディープリンク判定省略
+ switch deepLink {
+ case .fruitDetail(let id):
+ // .fruitListも追加する
+ navigationRouter.path = [.fruitList, .fruitDetail(id: id)]
+ }
+ })
}
}
感想
実用的なケースだと思うので是非みなさんのお役に立てればと思って書いてみました。
初投稿で拙い部分もあったかと思いますが、最後まで読んでいただきありがとうございました!