🚥
SwiftUI で Router を介してビューを呼び出す
はじめに
以下のようなモジュール構成でアプリを作成するときモジュール内で画面遷移を行おうとしてもモジュール間で循環参照になってしまいうまくいきません
そこで Router クラスを導入して DI 的に依存関係を解決します
モジュール構成は以下のようになります
使い方
はじめに利用シーンから
各 views モジュール内では Environment
から Router を取り出し、適宜ビューを要求します
import SwiftUI
import Routings
public struct ContentView: View {
@Environment(\.router) var router
public init() {}
public var body: some View {
List(items) { item in
NavigationLink {
router.route(for: Routings.Detail(id: id))
} label: {
Text("\(id)")
}
}
}
}
Routings
では以下のように対象のビューに必要なデータを持つバリューオブジェクトを列挙していきます
Routings/Routings.swift
public enum Routings {
public struct Detail: Routing {
public let id: Int
public init(id: Int) { self.id = id }
}
// etc...
}
// このような extension を用意しておくと SE-0299 により
// `router.route(for: .detail(id: id))` のように呼び出せる
extension Routing where Self == Routings.Detail {
public static func detail(id: Int) { self.init(id: id) }
}
実際のビューはアプリ本体側で RoutingProvider
を実装したクラスを用意し、各 Routing
の実態に対応するビューをマッピングします
それを AppDelegate
などで保持した Router
に渡し Environment
経由で参照できるようにセットします
App
import SwiftUI
import Routings
@MainActor
struct RouteProvider: RoutingProvider {
@ViewBuilder
func route(for target: any Routing) -> some View {
switch target {
case let detail as Routings.Detail:
DetailView(id: detail.id)
// case ...:
default:
Text("unknown routing: \(String(describing: target))")
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate {
private(set) lazy var router = Router(provider: RouteProvider())
}
struct App: SwiftUI.App {
@UIApplicationDelegateAdaptor var appDelegate: AppDelegate
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.router, appDelegate.router)
}
}
}
使い方に関しては以上です
また、 説明では NavigationLink
だけでしたが、 router.route(for:...)
は通常のビューを返すだけなので画面遷移だけでなく、ビュー内の要素を表示するモジュールのためにも利用できます
さらに、 iOS16 で登場する NavigationStack
に関してもこの実装だとバリューオブジェクトである Routing
をシームレスに NavigationPath
で利用できると思います
struct ContentView: View {
@State var navigationStacks = NavigationPath([Routings.List(), Routings.Detail(id: 1)])
var body {
NavigationStack(path: $navigationStacks) {
RootView()
.navigationDestination(for: Routings.List.self) { list in ... }
.navigationDestination(for: Routings.Detail.self) { detail in ... }
}
}
}
Routings 実装
Routings/Router.swift
import SwiftUI
// ※ Hashable 適合は実装に不要、 NavigationStack 利用の際はここにあると便利
public protocol Routing: Hashable {}
public protocol RoutingProvider {
associatedtype ResultView: View
@Sendable
@MainActor
@ViewBuilder
func route(for target: any Routing) -> ResultView
}
public struct Router: Sendable {
let router: @Sendable @MainActor (any Routing) -> AnyView
@MainActor
public func route(for target: some Routing) -> some View {
router(target)
}
}
extension Router {
public init<Provider: RoutingProvider>(provider: Provider) {
let route = provider.route(for:)
router = { target in
AnyView(route(target))
}
}
}
extension EnvironmentValues {
private struct Key: EnvironmentKey {
static var defaultValue: Router {
Router { _ in
AnyView(EmptyView())
}
}
}
public var router: Router {
get { self[Key.self] }
set { self[Key.self] = newValue }
}
}
Discussion