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:) and handleDeepLink(url:) methods to process incoming URLs
  • Managing optional sheet presentation state for modal flows
  • Offering navigate(to:) and reset() 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.

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
}

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.

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 RouterPath class that holds the [Route] array and handles URL parsing, as shown in swiftui-ui-patterns/references/deeplinks.md.
  • Use a Route enum to define all navigable destinations, enabling type-safe deep links and clean navigationDestination(for:) mappings.
  • Inject via withLinkRouter to wire OpenURLAction and .onOpenURL together, capturing both in-app and system-initiated URLs.
  • Support per-tab routing by creating separate RouterPath instances for each tab's NavigationStack, preserving independent navigation histories.
  • Validate URLs before navigation to distinguish internal deep links from external web links, returning .systemAction for the latter.

Frequently Asked Questions

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.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →