0
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【SwiftUI】NavigationStackのpathを使って、画面一覧を一挙に宣言して自動遷移させてみた

Last updated at Posted at 2024-04-25

NavigationStack

従来のNavigationViewはiOS16以降で非推奨となり、代わりにNavigationStackの導入が推奨されています。

基本的な使い方

NavigationViewと同じように使うことができます。

struct ContentView: View {
    var body: some View {
        NavigationStack {
            NavigationLink("次の画面へ") {
                NextView()
            }
        }
    }
}

NavigationPathnavigationDestinationを使った実装

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まで一挙に宣言してしまいます。
ルートのnavigationDestinationscreenKey.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を操作できるようにするためにNavigationStackenvironmentObjectに設定してしまいます。
これで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からの解放も嬉しい。
この実装方法では、ディープリンクや、ログイン状態による分岐、ログアウト処理などを直感的でシンプルに書くこともできるので別で記事にしたいと思います。
お役に立てれば幸いです!

追記

この実装方法では、ディープリンクや、ログイン状態による分岐、ログアウト処理などを直感的でシンプルに書くこともできるので別で記事にしたいと思います。

してみました!

0
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?