[SwiftUI] Routeで管理する

2 min read読了の目安(約2500字

背景

NavigationLinkで画面遷移をすると、

  • 画面ごとに遷移状態のフラグを持たなければならない
  • 毎回NavigationLinkを書かないといけない

のが非常に面倒だったので、 WebとかFlutterでもあるRoutingをEnvironmentObjectを使って実装しました。

実装

1. Routeを管理するクラスを用意

import Foundation

// Routeを管理するクラスを用意
class RouteState: ObservableObject {
    @Published var appRoute: Route = .splash
    @Published var routeHistory: [Route] = [.splash]
    
    func moveToView(route: Route) {
        routeHistory.append(appRoute)
        appRoute = route
    }
    
    func moveToBack() {
        appRoute = routeHistory.last ?? .splash
        routeHistory.removeLast()
    }
    
    func popUntil(route: Route) {
        let untilIndex = routeHistory.firstIndex(of: route) ?? 1
        appRoute = routeHistory[untilIndex]
        let lastIndex = routeHistory.endIndex
        routeHistory.removeSubrange(untilIndex..<lastIndex)
    }
}

2. EnvironmentObjectを紐付け

import SwiftUI

@main
struct RoutingSampleApp: App {
    var body: some Scene {
        WindowGroup {
            RouteView()
                .environmentObject(RouteState())
        }
    }
}

3. RouteViewでRouteを定義

import SwiftUI

enum Route {
    case splash
    case login
    case home
}

struct RouteView: View {
    @EnvironmentObject var routeState: RouteState
    var body: some View {
        switch routeState.appRoute {
        case .splash:
            Button(action: {
                routeState.moveToView(route: .login)
            }, label: {
                Text("ログイン画面へ")
            })
        case .login:
            Button(action: {
                routeState.moveToView(route: .home)
            }, label: {
                Text("ホーム画面へ")
            })
        case .home:
            VStack {
                Button(action: {
                    routeState.moveToBack()
                }, label: {
                    Text("ログイン画面へ")
                })
                Button(action: {
                    routeState.popUntil(route: .splash)
                }, label: {
                    Text("スプラッシュ画面へ")
                })
            }
            
        }
    }
}

メリット

  • NavigationLinkを各画面で定義しなくてよくなる
    • 画面遷移時はrouteStateのappRouteを変更するだけ
  • SpinnerやAlertなどをRouteViewに定義して使いまわせる
    • SpinnerStateやAlertStateなどを定義して、RouteViewに.sheetなどを定義する

デメリット

  • 画面遷移の遷移方法を設定できない(pushやmodalなど)
    • RouteViewで表示を切り替えているだけなので、今のままだとできない(RouteViewでNavigationLinkと組み合わせればできるかも・・)
  • 画面の値渡しがEnvironmentObjectを使わないといけない(もっといいやり方ありそう)
    • 画面の値渡し用の環境変数を用意して、前画面で代入、次画面で環境変数を参照という感じ

全体コード

https://github.com/PictoMki/RoutingSample