Building SwiftUI Navigation for a Modular App

Introduction In my previous post, I surveyed three approaches to SwiftUI navigation at scale: shared routers, navigation frameworks, and per-module coordinators. I explained the trade-offs of each. This post is the implementation. A demo project with five feature modules, a tab bar, a modal settings flow, an onboarding gate, deep linking, and unit tests. The architecture: Vertical feature modules that can’t see each other A generic Router per module that holds navigation state Each module composes its own views internally A single AppCoordinator for cross-module orchestration No coordinator per feature module The Module Structure App (composition root) ├── Home (tab — list → detail push) ├── Profile (tab — static screen, edit sheet) ├── Account (tab — logout action) ├── Onboarding (separate flow — experiment-based branching) ├── UserSettings (modal sheet) └── Navigation (shared — Router generic) Each feature package depends on Navigation but never on each other. The Navigation package contains only the generic Router. Pure infrastructure with no app-specific types. The App target is the only thing that imports everything. ...

March 18, 2026 · 11 min · Luke Jones

Three Ways to Solve SwiftUI Navigation at Scale

The Problem SwiftUI navigation doesn’t scale. NavigationStack works for simple apps. Define some destinations, register them with .navigationDestination(for:), push views onto a path. But the moment your app grows beyond a handful of screens, cracks appear. Views start deciding where to go next. Navigation logic gets duplicated. Sheets and full-screen covers are managed with scattered boolean bindings. Deep links have nowhere clean to land. And if your app is modular, split into separate Swift packages with independent feature teams, the question of “how does module A navigate to module B when they can’t import each other?” has no obvious answer. ...

March 17, 2026 · 5 min · Luke Jones