🐧

NavigationStack を使った動的な画面遷移の実装手順

2025/02/03に公開

概要

本記事では、NavigationStackのpathを活用し、動的に画面遷移を管理する方法を解説します。
アプリ全体で遷移管理を一元化するために、AppRouterというクラスを作成し、@Environmentを使って各Viewからアクセスできるようにしました。

サンプルコードは GitHub に公開していますので、ぜひご活用ください!🚀
https://github.com/tomoEng11/NavigationPractice

実装手順

1. Destinationの作成

まず、NavigationStackで使用する遷移先の画面(Destination)を管理するenumを作成します。
Destinationは、Hashableに準拠する必要があります。
また、画面によっては パラメータを渡す必要があるため、連想値を持たせることが可能です。

さらに、makeView()を定義することで、Destinationに応じたViewを動的に返す仕組みを作ります。

enum Destination: Hashable {
    case home
    case activity
    case profile(User)
    case settings

    @ViewBuilder
    func makeView() -> some View {
        switch self {
        case .home: HomeView()
        case .activity: ActivityView()
        case .profile(let user): ProfileView(user: user)
        case .settings: SettingsView()
        }
    }
}

2. AppRouterの作成

次に、アプリ全体の画面遷移を管理するAppRouterを作成します。
NavigationStackのpathに、配列操作のようにDestinationを追加・削除するだけで画面遷移が可能です。

@MainActor
@Observable
final class AppRouter {
    var path: [Destination] = []

    func push(_ destination: Destination) {
        path.append(destination)
    }

    // ルート画面に戻り、指定したdestinationに遷移
    func pushAndRemoveUntil(_ destination: Destination) {
        path.removeAll()
        path.append(destination)
    }

    // 現在のViewをdestinationに置き換えて遷移
    func pushReplacement(_ destination: Destination) {
        if !path.isEmpty {
            path.removeLast()
        }
        path.append(destination)
    }

    func pop() {
        if !path.isEmpty {
            path.removeLast()
        }
    }

    func popToRoot() {
        path.removeAll()
    }

    // 指定したdestinationまで戻る
    func popUntil(_ target: Destination) {
        while path.last != target && !path.isEmpty {
            path.removeLast()
        }
    }
}

3. View側でNavigationStackの実装

AppRouterを@Environment登録することで、アプリ全体で遷移管理が可能になります。
まず、App内で@Stateを使ってAppRouterを保持し、environmentモディファイアで子View(RootView)にrouterを渡します。

@main
struct NavigationPracticeApp: App {
    @State private var router: AppRouter = .init()
    var body: some Scene {
        WindowGroup {
            RootView()
                .environment(router)
        }
    }
}

struct RootView: View {
    @Environment(AppRouter.self) var router

    var body: some View {
        @Bindable var router = router

        NavigationStack(path: $router.path) {
            VStack(spacing: 40) {
                CustomButton("Home") {
                    router.push(.home)
                }

                CustomButton("Settings") {
                    router.push(.settings)
                }
            }
            .padding()
            .navigationDestination(for: Destination.self) { destination in
                destination.makeView()
            }
        }
    }
}

#Preview {
    RootView()
        .environment(AppRouter())
}

@Environment(AppRouter.self)を使えば
どのViewでもrouter.push(.xxx))のように簡単に遷移できます

struct HomeView: View {
    @Environment(AppRouter.self) var router

    var body: some View {
        VStack {
            CustomButton("Profile") {
                router.push(.profile(User(id: 1, name: "tomo")))
            }
        }
    }
}

#Preview {
    HomeView()
        .environment(AppRouter())
}

この方法を活用すれば、アプリの画面遷移がシンプルかつ柔軟になります!
サンプルコードでは、RootViewや特定の画面戻る実装もしてありますので、参考にしていただければと思います。
他にもいい画面遷移の実装があれば、ご指摘いただければ幸いです。

参考記事

https://qiita.com/yoshi-eng/items/91666637cd7cdd8edf88

Discussion