Architecture
Navigation
How I typed routes and pushed screens from ViewModels.
How I navigated in SwiftUI with AppRoute and a small router.
Discover uses NavigationStack, a typed route enum, and RouterProtocol. ViewModels call router.push(...) instead of hiding navigation inside Views.
AppRoute
Every push destination:
enum AppRoute: Hashable {
case home
case detail(poi: POI)
}
Hashable so SwiftUI stores it in a navigation path and resolves navigationDestination(for: AppRoute.self).
A sealed list of screens, not string URLs. The compiler knows .detail needs a POI.
Navigation path
Each tab has its own stack and router:
@State private var homeRouter = AppRouter()
@State private var favoritesRouter = AppRouter()
AppRouter holds:
var path: [AppRoute] = []
func push(_ route: AppRoute) {
guard path.last != route else { return }
path.append(route)
}
NavigationStack(path: $homeRouter.path) binds the stack. Two tabs = two independent paths.
What I did
AppRoute enum + AppRouter class + RouterProtocol for ViewModels.
Why (then)
I wanted ViewModels to trigger navigation without importing SwiftUI navigation APIs everywhere. Tests could record push(.detail(poi:)) with a mock router.
What I’d reconsider
AppRouterView got busy with two stacks, factories, and navigationDestination. I might extract a small coordinator per tab if routes multiply.
RouterProtocol
ViewModels depend on the protocol, not AppRouter:
protocol RouterProtocol {
func push(_ route: AppRoute)
func pop()
func popToRoot()
}
Wiring in AppRouterView
NavigationStack(path: $homeRouter.path) {
container.homeFactory.makeView(router: homeRouter, namespace: zoomNamespace)
.navigationDestination(for: AppRoute.self) { route in
switch route {
case .home:
container.homeFactory.makeView(router: homeRouter, namespace: zoomNamespace)
case .detail(let poi):
container.detailFactory.makeView(poi: poi)
}
}
}
Factory creates the root. navigationDestination maps each case to a View.
Could I navigate from the View only?
Yes. NavigationLink(value: poi) in the list, no router. Fine for simple flows. I pushed navigation into ViewModels to match MVVM habits from UIKit and to test “user tapped row → detail route” without ViewInspector.
Coordinator, MVVM-C, or Router?
UIKit teams often use a Coordinator to own navigation. SwiftUI literature mentions MVVM-C.
I used RouterProtocol + AppRouter: same idea, smaller surface for three screens. A full Coordinator tree might help with deep links and modal flows I have not built yet.