📛
iOS 16からのNavigation(画面遷移)のベストプラクティス(NavigationStack)
ベストプラクティスと言い切る
ベストプラクティスって言うと怖いんですが、勇気を持って言い切っています(前置き逃げ)
ベストプラクティス
@EnvironmentObject
とnavigationDestination(for:destination:)
を主に使用し構成していきます。
実装のポイント
1. NavigationのItemはenumで扱う
navigationDestination(for:destination:)
は遷移アイテムを全て設定しなければいけないのですが、設定し忘れてしまうと遷移できません。
そこでenumを使うことでより見通しの良く、設定し忘れなく遷移先を設定することができます。
@EnvironmentObject
で扱う()
2. NavigationPathをどの子Viewでも画面遷移する可能性はあり、全てのViewに対してBindingで渡すことはあまりにも不便です。
そこで@EnvironmentObject
を使うことで、大元で一度セットするだけで全ての子Viewで画面遷移が利用可能になります。
実装
final class NavigationRouter: ObservableObject {
@MainActor @Published var items: [Item] = []
enum Item: Hashable {
case sub1(id: Int)
case sub2(id: Int)
}
}
struct ContentView: View {
@StateObject var router = NavigationRouter()
var body: some View {
NavigationStack(path: $router.items) {
List {
Button("SubView1") {
router.items.append(.sub1(id: 1))
}
Button("SubView2") {
router.items.append(.sub2(id: 1))
}
}
.navigationDestination(for: NavigationRouter.Item.self) { item in
switch item {
case .sub1(id: let id):
SubView1(id: id)
.navigationTitle("SubView1(\(id))")
case .sub2(id: let id):
SubView2(id: id)
.navigationTitle("SubView2(\(id))")
}
}
.navigationTitle("Main")
}
// NavigationStackの中のViewに適用ではなく、NavigationStack自体に、environmentObjectを設定する
.environmentObject(router)
}
}
struct SubView1: View {
@EnvironmentObject var router: NavigationRouter
let id: Int
var body: some View {
List {
Button("SubView2") {
router.items.append(.sub2(id: id + 1))
}
Button("Back") {
router.items.removeLast()
}
}
}
}
struct SubView2: View {
@EnvironmentObject var router: NavigationRouter
let id: Int
var body: some View {
List {
Button("SubView1") {
router.items.append(.sub1(id: id + 1))
}
Button("Back") {
router.items.removeLast()
}
}
}
}
できないこと(ViewModelが画面遷移が持てない)
@EnvironmentObject
を使っているため、ViewModel
に画面遷移を持たせづらいです。
持たせづらいだけでできない事はないです。
final class SubView3Model: ObservableObject {
private func doSomething1() throws { }
func doSomething(push: (NavigationRouter.Item) -> ()) {
do {
try doSomething1()
push(.sub1(id: 1))
try doSomething2()
} catch {
print(error)
}
}
}
struct SubView3: View {
@EnvironmentObject var router: NavigationRouter
@StateObject var viewModel = SubView3Model()
var body: some View {
Button("DO SOMETHING") {
viewModel.doSomething { item in
router.items.append(item)
}
}
}
}
Tips 1: ViewModelの初期化
アプリのアーキテクチャでViewModel的なものを採用している場合switch case
内でViewModelの初期化が可能です。
.navigationDestination(for: NavigationRouter.Item.self) { item in
switch item {
...
case .sub4(id: let id):
// ここでViewModelの初期化
let viewModel = SubView4Model(id: id)
SubView4(viewModel: viewModel)
.navigationTitle("SubView4(\(id))")
}
}
final class SubView4Model: ObservableObject {
let id: Int
@Published var isPresented: Bool
init(id: Int) {
self.id = id
self.isPresented = false
}
}
struct SubView4: View {
@EnvironmentObject var router: NavigationRouter
@StateObject var viewModel: SubView4Model
var body: some View {
....
}
}
Discussion
こんにちは。参考になる記事をありがとうございます。
質問ですが、遷移先のViewに@Bindingがあった場合、どのように変数を渡すのが良いと思いますか?
一応ItemにBinding<T>を持たせることで
@Binding
を渡すことができました。ただBinding自体が
Hashable
に準拠していないので、ItemをHashableに準拠させるために、こういったextensionを用意しておくといいかもしれません。(全てのケースでうまく動くか微妙ですが...)返信ありがとうございました。
家に帰ったら、挑戦してみます。