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

SwiftUI Navigation with Coordinators

SwiftUI Navigation with Coordinators: Scaling NavigationStack Without Losing Your Mind Introduction NavigationStack was released in iOS 16 and I was originally excited for it. For years before that, SwiftUI navigation felt like a collection of half measures: NavigationView, NavigationLink scattered through views, boolean bindings to trigger pushes, and a long list of weird hacks just to present screens conditionally. When things became truly complex, we inevitably fell back to UIHostingController, dragging UIKit back into places where SwiftUI was supposed to shine. ...

February 10, 2026 · 5 min · Luke Jones

Swift Concurrency: Actors

In the last posts we looked at running work in parallel with async let and TaskGroup, and then how Sendable ensures the data we share across tasks is safe. The next piece of Swift’s structured concurrency story is actors. Actors provide isolation for mutable state, making them one of the core building blocks for avoiding data races. What is an Actor? An actor is like a class, but with one key difference: only one task at a time can interact with its mutable state. This guarantees thread safety without you having to add locks manually. ...

December 17, 2025 · 3 min · Luke Jones

Swift Concurrency: Sendable

In the previous post, we looked at running work in parallel with async let and TaskGroup. But once tasks start running in parallel, there’s a bigger question: what happens to the data they share? Swift’s answer is the Sendable protocol. What is Sendable? Sendable is a marker protocol that tells the compiler a type can safely be passed across concurrency domains like tasks and actors. If a type isn’t Sendable, Swift will stop you from moving it into a concurrent context because that might cause a data race. The compiler does a lot of this checking automatically, but sometimes you’ll need to be explicit. ...

December 15, 2025 · 3 min · Luke Jones

Swift Concurrency: Parallelism

In the previous post, we loaded three independent requests sequentially. Even though the operations didn’t depend on each other, each one waited for the previous to finish To run them in parallel while keeping structured concurrency, Swift gives us two main tools: async let and TaskGroup. When to use async let Let’s start with async let. It allows you to fire off multiple operations in parallel within the same task context: ...

December 12, 2025 · 5 min · Luke Jones

Swift Concurrency: From Closures to async/await

One of the biggest changes in Swift since I’ve been away is how we handle concurrency. Back in iOS 15, we got our first taste of Swift Concurrency with async/await. At the time, most of us were still relying heavily on Combine or traditional closures to handle asynchronous work. I had played around with it a little back then, but its practical use was limited because it couldn’t be deployed widely on older iOS versions. ...

December 10, 2025 · 3 min · Luke Jones

Testing Apple Sign-In Framework

Back in October 2019, I was assigned the task of implementing Apple Sign-in, a new single sign-on solution released by Apple as part of the release of iOS 13. It wasn’t too complicated to implement. Although, testing this framework certainly comes with its own set of challenges. Let’s take a look at how I implemented it. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class AppleSignInAuthenticationWrapper: NSObject { weak var delegate: AppleSignInAuthenticationDelegate? func signIn() { let provider = ASAuthorizationAppleIDProvider() let request = provider.createRequest() request.requestedScopes = [.fullName, .email] let controller = ASAuthorizationController(authorizationRequests: [request]) controller.delegate = self controller.presentationContextProvider = self controller.performRequests() } } In one single method call, we initialise the provider, create the request, inject the request into the controller and then call the performRequests method against the controller. That’s a lot of creation for one single method. ...

January 5, 2021 · 5 min · Luke Jones