How to Set Up Deep Link Routing in SwiftUI Applications: The Central Router Pattern
Deep link routing in SwiftUI requires a central router class—such as RouterPath from the Dimillian/Skills repository—that owns the navigation state, handles incoming URLs via OpenURLAction, and drives NavigationStack destinations programmatically.
Implementing robust deep link routing in SwiftUI means moving navigation logic out of views and into a dedicated router. According to the Dimillian/Skills repository, which catalogs proven SwiftUI UI patterns, the recommended architecture uses a RouterPath class to maintain a single source of truth for navigation paths while intercepting system URLs through environment actions. This approach eliminates tight coupling between views and navigation state, making your deep link routing testable and scalable across complex tab-based interfaces.
Core Architecture Components
The Skills repository defines a clear separation of concerns across four primary components. Each plays a specific role in the deep link chain, from URL interception to view presentation.
RouterPath: The Navigation State Owner
The RouterPath class (defined in swiftui-ui-patterns/references/deeplinks.md) serves as the brain of your navigation system. It maintains the path array that powers NavigationStack and exposes entry points for URL handling.
Key responsibilities include:
- Holding the
[Route]array that represents the current navigation stack - Providing
handle(url:)andhandleDeepLink(url:)methods to process incoming URLs - Managing optional sheet presentation state for modal flows
- Offering
navigate(to:)andreset()methods to programmatically control the stack
Route Enum: Type-Safe Destinations
The Route enum (documented in swiftui-ui-patterns/references/navigationstack.md) provides a Hashable representation of every navigable screen in your app. This enum is what the NavigationStack uses to determine which view to render.
enum Route: Hashable {
case account(id: String)
case status(id: String)
}
Using an enum ensures compile-time safety for deep links and allows navigationDestination(for:) to map routes to specific views exhaustively.
NavigationStack Binding
Each NavigationStack binds directly to a RouterPath instance's path property. This binding enables programmatic navigation simply by appending to the array, which is exactly what happens when a deep link is processed.
OpenURLAction and onOpenURL
The system integration happens through OpenURLAction and the .onOpenURL modifier. The Skills repository implements this through a custom withLinkRouter view modifier that injects both the environment action and the URL handler into the view hierarchy.
Implementing the Router
Creating the RouterPath Class
According to swiftui-ui-patterns/references/deeplinks.md, your router should be marked as @MainActor since it updates UI state. Here's the canonical implementation:
@MainActor
final class RouterPath {
var path: [Route] = []
var urlHandler: ((URL) -> OpenURLAction.Result)?
func handle(url: URL) -> OpenURLAction.Result {
if isInternal(url) {
navigate(to: .status(id: url.lastPathComponent))
return .handled
}
return urlHandler?(url) ?? .systemAction
}
func handleDeepLink(url: URL) -> OpenURLAction.Result {
// Resolve federated URLs, then navigate
navigate(to: .status(id: url.lastPathComponent))
return .handled
}
func navigate(to route: Route) {
path.append(route)
}
func reset() {
path = []
}
private func isInternal(_ url: URL) -> Bool {
// Example: only handle myapp:// scheme
return url.scheme == "myapp"
}
}
The isInternal(_:) helper validates the URL scheme before treating it as an in-app link. If validation fails, the router returns .systemAction to let iOS handle the URL externally.
Defining the Route Enum
From swiftui-ui-patterns/references/navigationstack.md, define all possible destinations as enum cases with associated values for parameters:
enum Route: Hashable {
case account(id: String)
case status(id: String)
// Add additional destinations here
}
Wiring Deep Links into SwiftUI
The withLinkRouter View Modifier
The critical integration point is the withLinkRouter modifier defined in swiftui-ui-patterns/references/deeplinks.md. This modifier does two things: it injects a custom OpenURLAction into the environment to handle openURL calls from within the app, and it adds .onOpenURL to handle system-initiated deep links.
extension View {
func withLinkRouter(_ router: RouterPath) -> some View {
self
.environment(\.openURL, OpenURLAction { url in
router.handle(url: url)
})
.onOpenURL { url in
router.handleDeepLink(url: url)
}
}
}
App-Level Integration
Apply the modifier at your app's entry point to ensure the router is available throughout the view hierarchy:
@main
struct MyApp: App {
@StateObject private var router = RouterPath()
var body: some Scene {
WindowGroup {
ContentView()
.withLinkRouter(router)
}
}
}
Now when the system opens a URL associated with your app, the OpenURLAction receives it first, forwards it to router.handle(url:), and updates the navigation path accordingly.
Multi-Tab Navigation Setup
For apps with multiple tabs, the Skills repository recommends giving each tab its own RouterPath instance. This preserves independent navigation histories per tab, as documented in swiftui-ui-patterns/references/navigationstack.md:
@MainActor
struct TabsView: View {
@State private var timelineRouter = RouterPath()
@State private var notificationsRouter = RouterPath()
var body: some View {
TabView {
NavigationStack(path: $timelineRouter.path) {
TimelineView()
.navigationDestination(for: Route.self) { route in
switch route {
case .account(let id):
AccountView(id: id)
case .status(let id):
StatusView(id: id)
}
}
}
.environment(timelineRouter)
.tabItem { Label("Timeline", systemImage: "clock") }
NavigationStack(path: $notificationsRouter.path) {
NotificationsView()
.navigationDestination(for: Route.self) { route in
// Handle routes for notifications tab
}
}
.environment(notificationsRouter)
.tabItem { Label("Alerts", systemImage: "bell") }
}
}
}
Each NavigationStack binds to its respective router's path, ensuring that deep links route to the correct tab without losing state when switching contexts.
Handling External vs Internal Links
Your router should gracefully handle URLs that don't belong to your app. The pattern from swiftui-ui-patterns/references/deeplinks.md uses the urlHandler fallback:
func handle(url: URL) -> OpenURLAction.Result {
if isInternal(url) {
navigate(to: .status(id: url.lastPathComponent))
return .handled
}
// Falls back to system behavior (Safari, etc.)
return urlHandler?(url) ?? .systemAction
}
This approach ensures that shared web links open in Safari while custom scheme URLs (myapp://) navigate internally.
Summary
- Centralize navigation state in a
RouterPathclass that holds the[Route]array and handles URL parsing, as shown inswiftui-ui-patterns/references/deeplinks.md. - Use a
Routeenum to define all navigable destinations, enabling type-safe deep links and cleannavigationDestination(for:)mappings. - Inject via
withLinkRouterto wireOpenURLActionand.onOpenURLtogether, capturing both in-app and system-initiated URLs. - Support per-tab routing by creating separate
RouterPathinstances for each tab'sNavigationStack, preserving independent navigation histories. - Validate URLs before navigation to distinguish internal deep links from external web links, returning
.systemActionfor the latter.
Frequently Asked Questions
What is the best way to handle deep links in SwiftUI?
The most robust method is using a central router class like RouterPath that owns the navigation state and integrates with NavigationStack. This pattern, documented in swiftui-ui-patterns/references/deeplinks.md, separates navigation logic from views and allows you to handle URLs by simply appending values to a path array, which SwiftUI observes automatically.
How do I maintain separate navigation stacks for each tab?
Create a separate RouterPath instance for each tab using @State at the parent level, as demonstrated in swiftui-ui-patterns/references/navigationstack.md. Bind each NavigationStack to its respective router's path property, and inject the router into the tab's environment. This ensures each tab maintains its own history independently.
Can I test deep link routing without running the full app?
Yes. Because RouterPath is a plain class annotated with @MainActor, you can instantiate it directly in unit tests. Call handle(url:) or handleDeepLink(url:) with test URLs and assert that the path array contains the expected Route values. This testability is a key advantage of the router pattern over navigation driven by view-layer state.
How do I handle external URLs that aren't part of my app?
Validate URLs using a helper like isInternal(_:) that checks the scheme or host. If validation fails, return .systemAction from your OpenURLAction handler. This tells iOS to open the URL in Safari or the appropriate external app, as implemented in the handle(url:) method within swiftui-ui-patterns/references/deeplinks.md.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →