Jacob’s Tech Tavern

Jacob’s Tech Tavern

Handle Deep Links with Async Algorithms

Building a reactive system with Swift Concurrency

Jacob Bartlett's avatar
Jacob Bartlett
Mar 24, 2025

Deep links are mandatory if you want users engaging with your app. Alongside push notifications, deep links form the technical cornerstone of your user retention strategy: a user gets a notification, taps it, then expects to be brought to the relevant screen in your app.


Subscribe to Jacob’s Tech Tavern for free to get ludicrously in-depth articles on iOS, Swift, tech, & indie projects in your inbox every week.

Upon seeing my Canva draft, my toddler asked to be in the image with me. How could I refuse her?

you aren’t smashing out greenfield projects on the daily, you might never have set up a deep link handler. You’ll often add new routes at the whims of your product & marketing teams, however the handler itself is built once and injected across your app’s navigation architecture.

Async Algorithms is a bundle of algorithms built on top of Swift Concurrency. They handle sequences of values over time—making it the perfect fit for setting up a reactive, responsive, and ergonomic deep linking infrastructure throughout your app.

Let’s look at how we can leverage Async Algorithms to easily implement deep linking in your projects today.

Can’t wait? Check out my open-source project to see the whole thing in action now.

Async Algorithms

Let’s quickly get to grips with the basics of this package.

Async Algorithms, first released in 2022, brought much of the reactive ergonomics of Combine to the modern world of Swift Concurrency. There’s just one protocol you need to understand to really get to grips with Async Algorithms: AsyncSequence.

AsyncSequence works a lot like the regular Sequence protocol in the Swift Standard Library: it provides an asynchronous version of the Iterator protocol that offers async iterated access to the next() element. This iteration can be invoked using the for-await-in syntax sugar.

For a (very) detailed primer, feel free to read my very detailed tutorial about migrating to the package from Combine.

Migrating Combine to AsyncAlgorithms

Jacob Bartlett
·
October 7, 2024
Migrating Combine to AsyncAlgorithms

Subscribe to Jacob’s Tech Tavern for free to get ludicrously in-depth articles on iOS, Swift, tech, & indie projects in your inbox every week.

Read full story

Setting up Deep Links

If you haven’t set up deep linking in your apps before, it’s fairly straightforward, only requiring a few steps.

First, create a URL type in your Info.plist. This will tell iOS to map a simple customised URL scheme like linky:// to your full bundle identifier, com.jacob.Linky and open your app.

In production, you will likely also set up universal links. These are more complex to set up, but work the same way in your app. The mapping to your app ID is provided by the apple-app-site-association file on your web domain.

This is dead simple to test using the Simulator. Just launch the app from Xcode and run this terminal command:

xcrun simctl openurl booted 'linky://'

Then your app should open up handily.

We will be doing a lot of debugging as we build this.

If you want to use breakpoints to debug your deep links, go to the scheme editor and check “wait for the executable to be launched”.

This allows your app to build without launching, so the deep linking terminal command opens the app in debug mode.

Our Architecture

This sample project uses my favoured architecture for mid-size SwiftUI projects, but the Async Algorithms approach for the deep link handler can be easily ported to any iOS app, so don’t worry if you use NavigationStack, TCA, Navigator, or you roll your own custom approach.

In short, I used good old-fashioned UIKit navigation with Coordinators, where my SwiftUI were wrapped in UIHostingControllers. For more on this architecture, check out this article piece based my NSLondon talk:

SwiftUI apps at scale

Jacob Bartlett
·
July 4, 2023
SwiftUI apps at scale

SwiftUI is fantastic, but not perfect. Some of these imperfections are expected from a nascent 4-year-old framework, but just one creates the roadblock to wider adoption.

Read full story

The SceneDelegate

One last bit of setup before we go into the Async Algorithms.

Sorry for the long-winded intro! No dev left behind.

The SceneDelegate serves as our entry point for all deep links.

import UIKit
import Factory

final class SceneDelegate: UIResponder, UIWindowSceneDelegate {    
    @Injected(\.deepLinkHandler) private var deepLink
    var window: UIWindow?
    var appCoordinator: AppCoordinator?
}

I’m opinionatedly using Factory to dependency-inject my deep link handler but you can roll whatever you want. You don’t need to understand it for this article.

When the app cold-launches, it will call willConnectTo. This is where you perform initial app navigation setup and handle any deep links that caused launch inside connectionOptions.urlContexts.

func scene(
    _ scene: UIScene,
    willConnectTo session: UISceneSession,
    options connectionOptions: UIScene.ConnectionOptions
) {
    guard let windowScene = (scene as? UIWindowScene) else { return }
        
    window = UIWindow(windowScene: windowScene)
    appCoordinator = AppCoordinator(window: window!)
    appCoordinator?.start()
        
    if let urlContext = connectionOptions.urlContexts.first {
        Task { await deepLink.open(url: urlContext.url) }
    }
}

We’ll look into this deepLink.open(url:) method momentarily.

We also want to add entry-points for deep links using openURLContexts when the app is already alive:

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
    guard let urlContext = URLContexts.first else { return }
    
    Task {
        await deepLink.open(url: urlContext.url)
    }
}

And also the continue userActivity flavour when using a universal link:

func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
    guard let url = userActivity.webpageURL else { return }
    
    Task {
        await deepLink.open(url: url)
    }
}

In a pure-SwiftUI app, you may leverage an onOpenURL modifier high up in your app navigation hierarchy, just inside the window group. Here, you can invoke the deep link handler from the URL in the exact same way:

@main
struct LinkyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .onOpenURL { url in
                    Task {
                        await deepLink.open(url: url)
                    }
                }
        }
    }
}

While in active development, it’s often easiest to hardcode a deeplink

func scene(
    _ scene: UIScene,
    willConnectTo session: UISceneSession,
    options connectionOptions: UIScene.ConnectionOptions
) {
      // ...

//    if let urlContext = connectionOptions.urlContexts.first {
//        Task { await deepLink.open(url: urlContext.url) }
//    }

    Task { 
        await deepLink.open(url: URL(string: "linky://addcontact")!) 
    }    
}

Let’s get started with our deep link handler!

DeepLinks

As with most utilities in Swift, we start with a protocol.

import AsyncAlgorithms
import Foundation

public protocol DeepLinkHandler {
    func open(url: URL) async
    func stream(_ link: DeepLink) -> AsyncChannel<DeepLink>
}

This can be injected into our SceneDelegate and Coordinators.

We’ve already seen how we open URLs.

The stream(link:) method is where the magic happens. We set up listeners in each of our coordinators, where each one listens for the relevant links.

DeepLink is an enumeration of every possible deep link we handle in the app.

public enum DeepLink: String, CaseIterable, Sendable {
    
    case favourites = #"/favourites/?$"#
    case recents = #"/recents/?$"#
    case mostRecent = #"/mostrecent/?$"#
    case contacts = #"/contacts/?$"#
    case addContact = #"/addcontact/?$"#
    
    fileprivate static func link(from url: URL) -> DeepLink? {
        DeepLink.allCases.first(
            where: { url.absoluteString ~= $0.rawValue }
        )
    }
}

We use a simple regex to map URL strings into the enum case.

infix operator ~= : ComparisonPrecedence

static func ~= (lhs: String, rhs: String) -> Bool {
    do {
        let regex = try NSRegularExpression(pattern: rhs, options: [])
        let range = NSRange(lhs.startIndex..<lhs.endIndex, in: lhs)
        return regex.firstMatch(in: lhs, options: [], range: range) != nil
    } catch {
        return false
    }
}

Deep Link URL Handler

Now let’s implement our deep linking protocol.

import AsyncAlgorithms

public final class DeepLinkHandlerImpl: DeepLinkHandler {    
    private let _favouritesStream = AsyncChannel<DeepLink>()
    private let _contactsStream = AsyncChannel<DeepLink>()
    private let _addContactStream = AsyncChannel<DeepLink>()
    private let _recentsStream = AsyncChannel<DeepLink>()
    private let _mostRecentStream = AsyncChannel<DeepLink>()
}

AsyncChannel powers our deep linking navigation listeners.

This is an AsyncAlgorithm entity heavily inspired by Combine subjects. It’s a special AsyncSequence which applies back pressure — this means it’ll buffer values. It wait for a value to be consumed downstream before calling the next() function on its internal iterator.

This makes it perfect for handling our deep link URLs. We can parse out the DeepLink enum case on open(url:) and forward it to the relevant channel.

public func open(url: URL) async {
    guard let link = DeepLink.link(from: url) else { return }
    Task {
        switch link {
        case .favourites:
            await _favouritesStream.send(link)
        case .contacts:
            await _contactsStream.send(link)
        case .addContact:
            await _addContactStream.send(link)
        case .recents:
            await _recentsStream.send(link)
        case .mostRecent:
            await _mostRecentStream.send(link)
        }
    }
}

Now, as soon as a valid deep link URL is opened, it passes the DeepLink case to the associated channel.

Subscribe for free to continue reading

Enter your email and get every post from Jacob’s Tech Tavern.
Already have an account? Sign in
© 2026 Jacob Bartlett · Privacy ∙ Terms ∙ Collection notice
Start your SubstackGet the app
Substack is the home for great culture