SwiftUI Deep Linking & URL Routing
Sebastien Lato

Sebastien Lato @sebastienlato

About: iOS & full-stack developer building polished SwiftUI apps, React/Next.js projects, and indie tools under the LatoDev brand.

Joined:
Nov 24, 2025

SwiftUI Deep Linking & URL Routing

Publish Date: Dec 8 '25
0 0

Deep linking is what turns a normal app into a connected system:

  • tap a link → open a specific screen
  • push a notification → navigate into a flow
  • share a URL → recipient opens the same state
  • run a shortcut → perform an action in-app

But SwiftUI's navigation system changed a lot between iOS 15 → iOS 18 → iOS 20.

Here’s the clean, modern way to handle deep links using:

  • NavigationStack
  • global app state
  • dependency injection
  • URL parsing
  • scene-based handlers

This is production-grade deep linking — not toy examples.


🧠 1. Define Your Deep Link Model

Start by defining all linkable destinations:

enum DeepLink: Equatable {
    case profile(userID: String)
    case settings
    case feed(category: String)
}
Enter fullscreen mode Exit fullscreen mode

This becomes the “navigation language” of your app.


🌐 2. Parse URLs Into DeepLink Types

Create a simple parser:

struct DeepLinkParser {
    static func parse(_ url: URL) -> DeepLink? {
        let components = URLComponents(url: url, resolvingAgainstBaseURL: true)

        switch components?.host {
        case "profile":
            if let id = components?.queryItems?.first(where: { $0.name == "id" })?.value {
                return .profile(userID: id)
            }
        case "settings":
            return .settings
        case "feed":
            if let category = components?.queryItems?.first(where: { $0.name == "category" })?.value {
                return .feed(category: category)
            }
        default:
            break
        }

        return nil
    }
}
Enter fullscreen mode Exit fullscreen mode

Supported example URLs:

myapp://profile?id=123
myapp://settings
myapp://feed?category=tech
Enter fullscreen mode Exit fullscreen mode

🔗 3. AppState Stores the Active Deep Link

@Observable
class AppState {
    var deepLink: DeepLink?
}
Enter fullscreen mode Exit fullscreen mode

Inject this state at the root:

@main
struct MyApp: App {
    @State var appState = AppState()

    var body: some Scene {
        WindowGroup {
            RootView()
                .environment(appState)
        }
        .onOpenURL { url in
            appState.deepLink = DeepLinkParser.parse(url)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

🧭 4. Central Navigation Handler

Inside your root navigation view:

struct RootView: View {
    @Environment(AppState.self) private var appState
    @State private var path: [DeepLink] = []

    var body: some View {
        NavigationStack(path: $path) {
            HomeView()
                .navigationDestination(for: DeepLink.self) { link in
                    destination(for: link)
                }
        }
        .onChange(of: appState.deepLink) { link in
            guard let link else { return }
            handle(link)
        }
    }

    func handle(_ link: DeepLink) {
        path = [link] // resets + navigates
    }

    @ViewBuilder
    func destination(for link: DeepLink) -> some View {
        switch link {
        case .profile(let id):
            ProfileView(userID: id)
        case .settings:
            SettingsView()
        case .feed(let category):
            FeedView(category: category)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This is the modern way:

  • navigation lives in a single place
  • deep link → navigation path
  • feature views stay clean and unaware of URLs

📲 5. Use Deep Links From Notifications

.onReceive(NotificationCenter.default.publisher(for: .deepLinkTrigger)) { notif in
    if let url = notif.object as? URL {
        appState.deepLink = DeepLinkParser.parse(url)
    }
}
Enter fullscreen mode Exit fullscreen mode

Triggering:

NotificationCenter.default.post(
    name: .deepLinkTrigger,
    object: URL(string: "myapp://profile?id=999")!
)
Enter fullscreen mode Exit fullscreen mode

Great for:

  • remote notifications
  • background pushes
  • Spotlight shortcuts
  • Siri shortcuts

📥 6. Deep Linking From Inside the App (Shared Links)

Create a sharable URL anywhere:

func share(userID: String) -> URL {
    URL(string: "myapp://profile?id=\(userID)")!
}
Enter fullscreen mode Exit fullscreen mode

Perfect for referral flows, invites, or user-to-user sharing.


🛡 7. Safe Deep Link Guarding

Prevent navigation when the user is not logged in:

func handle(_ link: DeepLink) {
    if !session.isLoggedIn {
        path = [.settings] // redirect
        return
    }

    path = [link]
}
Enter fullscreen mode Exit fullscreen mode

Production apps always guard deep links.


🧩 8. Deep Linking & Dependency Injection

Example: a feed requires a service:

FeedView(
    viewModel: FeedViewModel(
        category: category,
        service: environment.feedService
    )
)
Enter fullscreen mode Exit fullscreen mode

SwiftUI’s environment makes DI seamless.


🧪 9. Preview Testing Deep Link Navigation

#Preview("Profile Deep Link") {
    RootView()
        .environment(AppState(deepLink: .profile(userID: "preview")))
}
Enter fullscreen mode Exit fullscreen mode

You can now visually test navigation for every deep link.


🚀 Final Thoughts

Deep linking is the glue that connects:

  • external URLs
  • notifications
  • Siri shortcuts
  • cross-app flows
  • share links
  • web → app navigation

With the modern approach:

  • URLs become typed data (DeepLink)
  • navigation is centralized
  • features stay decoupled
  • app state drives routing
  • everything becomes predictable and testable

This transforms your SwiftUI app into a real, connected system — not just isolated screens.

Comments 0 total

    Add comment