Introduction

Deep linking connects the web and app worlds. Instead of dumping users on your app’s home screen, deep links take them exactly where they want to go—a specific product, article, user profile, or any other content.

For apps with marketing, sharing, or onboarding flows, deep linking is essential. This guide covers implementation from basics through advanced scenarios.

Deep Linking Fundamentals

What

Deep Linking Fundamentals Infographic Deep Links Do

Deep links are URLs that open specific content within your app:

Without deep linking:

  • User clicks link → App opens → Home screen → User manually navigates

With deep linking:

  • User clicks link → App opens → Specific content displayed immediately

Standard Deep Links (URI Schemes)

Custom URL schemes like myapp://product/123

  • Works when app is installed
  • Fails ungracefully when app isn’t installed
  • Not indexed by search engines
  • Simplest to implement

Universal Links (iOS) / App Links (Android)

Standard HTTPS URLs like https://myapp.com/product/123

  • Opens app if installed, website if not
  • Works across platforms
  • Search engine indexable
  • Requires server configuration

Deferred Deep Links

Links that persist through app installation:

  1. User clicks link (app not installed)
  2. User goes to app store
  3. User installs app
  4. App opens to correct content

Requires third-party service or custom implementation.

Implementing URI Schemes

iOS

Implementing URI Schemes Infographic Setup

1. Register URL Scheme

In Xcode, select your target → Info → URL Types:

  • Add new URL Type
  • Set identifier (e.g., com.yourcompany.yourapp)
  • Set URL scheme (e.g., yourapp)

Or in Info.plist:

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>yourapp</string>
        </array>
        <key>CFBundleURLName</key>
        <string>com.yourcompany.yourapp</string>
    </dict>
</array>

2. Handle Incoming Links

In your AppDelegate:

func application(_ app: UIApplication,
                 open url: URL,
                 options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {

    // Parse the URL
    guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
        return false
    }

    // Route based on path
    switch components.host {
    case "product":
        if let productId = components.queryItems?.first(where: { $0.name == "id" })?.value {
            navigateToProduct(id: productId)
            return true
        }
    case "profile":
        if let userId = components.queryItems?.first(where: { $0.name == "id" })?.value {
            navigateToProfile(id: userId)
            return true
        }
    default:
        break
    }

    return false
}

For SwiftUI apps using App protocol:

@main
struct YourApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .onOpenURL { url in
                    handleDeepLink(url: url)
                }
        }
    }
}

Android Setup

1. Define Intent Filters

In AndroidManifest.xml:

<activity android:name=".MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data
            android:scheme="yourapp"
            android:host="product" />
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data
            android:scheme="yourapp"
            android:host="profile" />
    </intent-filter>
</activity>

2. Handle Incoming Links

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        handleIntent(intent)
    }

    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        intent?.let { handleIntent(it) }
    }

    private fun handleIntent(intent: Intent) {
        val action = intent.action
        val data = intent.data

        if (action == Intent.ACTION_VIEW && data != null) {
            when (data.host) {
                "product" -> {
                    val productId = data.getQueryParameter("id")
                    productId?.let { navigateToProduct(it) }
                }
                "profile" -> {
                    val userId = data.getQueryParameter("id")
                    userId?.let { navigateToProfile(it) }
                }
            }
        }
    }
}

Testing URI Schemes

iOS Simulator:

xcrun simctl openurl booted "yourapp://product?id=123"

Android Emulator:

adb shell am start -W -a android.intent.action.VIEW -d "yourapp://product?id=123"

Universal Links (iOS) & App Links (Android) Infographic

Universal Links and App Links use standard HTTPS URLs, providing better user experience and fallback behaviour.

1. Configure Your Domain

Create apple-app-site-association file at https://yourdomain.com/.well-known/apple-app-site-association:

{
    "applinks": {
        "apps": [],
        "details": [
            {
                "appID": "TEAMID.com.yourcompany.yourapp",
                "paths": [
                    "/product/*",
                    "/profile/*",
                    "/share/*"
                ]
            }
        ]
    }
}

Requirements:

  • Must be served over HTTPS
  • Must have valid SSL certificate
  • No redirects
  • Content-Type: application/json

2. Enable Associated Domains

In Xcode → Signing & Capabilities → Add “Associated Domains”:

applinks:yourdomain.com

3. Handle Links

func application(_ application: UIApplication,
                 continue userActivity: NSUserActivity,
                 restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {

    guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
          let url = userActivity.webpageURL else {
        return false
    }

    return handleUniversalLink(url: url)
}

func handleUniversalLink(url: URL) -> Bool {
    guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
        return false
    }

    let pathComponents = components.path.split(separator: "/")

    if pathComponents.first == "product", pathComponents.count > 1 {
        let productId = String(pathComponents[1])
        navigateToProduct(id: productId)
        return true
    }

    return false
}

1. Configure Your Domain

Create assetlinks.json at https://yourdomain.com/.well-known/assetlinks.json:

[{
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
        "namespace": "android_app",
        "package_name": "com.yourcompany.yourapp",
        "sha256_cert_fingerprints": [
            "YOUR_APP_SIGNING_CERTIFICATE_SHA256_FINGERPRINT"
        ]
    }
}]

Get your fingerprint:

keytool -list -v -keystore your-keystore.jks -alias your-alias

2. Configure Intent Filters

<activity android:name=".MainActivity">
    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data
            android:scheme="https"
            android:host="yourdomain.com"
            android:pathPrefix="/product" />
    </intent-filter>
</activity>

The android:autoVerify="true" triggers verification against assetlinks.json.

3. Handle Links

Same as URI scheme handling—check the Intent data in your activity.

iOS:

  • Links must be clicked from another app (Safari, Notes, Messages)
  • Long-press to see “Open in [App]” option
  • Check Apple’s validation: https://app-site-association.cdn-apple.com/a/v1/yourdomain.com

Android:

adb shell am start -W -a android.intent.action.VIEW -d "https://yourdomain.com/product/123"

Check verification status:

adb shell pm get-app-links com.yourcompany.yourapp

Deferred Deep Linking

Deferred deep links work when the app isn’t installed, persisting through the installation process.

The Challenge

When a user without your app clicks a link:

  1. They’re sent to the app store
  2. They install the app
  3. The original link context is lost
  4. App opens to home screen instead of intended content

Solutions

Firebase Dynamic Links

Firebase’s solution for deferred deep linking:

// iOS
import FirebaseDynamicLinks

func application(_ application: UIApplication,
                 continue userActivity: NSUserActivity,
                 restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {

    let handled = DynamicLinks.dynamicLinks().handleUniversalLink(userActivity.webpageURL!) { dynamicLink, error in
        if let dynamicLink = dynamicLink, let url = dynamicLink.url {
            self.handleDeepLink(url: url)
        }
    }

    return handled
}

// Check for deferred link on app launch
DynamicLinks.dynamicLinks().getAppDynamicLink { dynamicLink, error in
    if let dynamicLink = dynamicLink, let url = dynamicLink.url {
        self.handleDeepLink(url: url)
    }
}

Branch.io

Commercial deep linking platform with advanced features:

// iOS
import Branch

func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    Branch.getInstance().initSession(launchOptions: launchOptions) { params, error in
        if let params = params as? [String: AnyObject] {
            self.handleBranchParams(params)
        }
    }

    return true
}

AppsFlyer OneLink

Attribution platform with deep linking:

// iOS
import AppsFlyerLib

func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    AppsFlyerLib.shared().appsFlyerDevKey = "YOUR_DEV_KEY"
    AppsFlyerLib.shared().appleAppID = "YOUR_APP_ID"
    AppsFlyerLib.shared().delegate = self

    return true
}

func onConversionDataSuccess(_ conversionInfo: [AnyHashable: Any]) {
    if let deepLinkValue = conversionInfo["deep_link_value"] as? String {
        handleDeepLink(value: deepLinkValue)
    }
}

Choosing a Solution

FeatureFirebaseBranchAppsFlyer
PriceFreeFreemiumPaid
Deferred deep linksYesYesYes
AttributionBasicAdvancedAdvanced
AnalyticsFirebase integrationBuilt-inBuilt-in
ComplexityModerateModerateHigher

For most apps, Firebase Dynamic Links provides sufficient functionality at no cost.

Building a Router

Create a centralised router for handling all deep links:

enum DeepLink {
    case product(id: String)
    case profile(id: String)
    case category(name: String)
    case search(query: String)
    case unknown

    init(url: URL) {
        guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
            self = .unknown
            return
        }

        let pathComponents = components.path
            .split(separator: "/")
            .map(String.init)

        switch pathComponents.first {
        case "product":
            if pathComponents.count > 1 {
                self = .product(id: pathComponents[1])
            } else {
                self = .unknown
            }
        case "profile":
            if pathComponents.count > 1 {
                self = .profile(id: pathComponents[1])
            } else {
                self = .unknown
            }
        case "category":
            if pathComponents.count > 1 {
                self = .category(name: pathComponents[1])
            } else {
                self = .unknown
            }
        case "search":
            let query = components.queryItems?.first(where: { $0.name == "q" })?.value ?? ""
            self = .search(query: query)
        default:
            self = .unknown
        }
    }
}

class DeepLinkRouter {
    func route(to deepLink: DeepLink) {
        switch deepLink {
        case .product(let id):
            navigateToProduct(id: id)
        case .profile(let id):
            navigateToProfile(id: id)
        case .category(let name):
            navigateToCategory(name: name)
        case .search(let query):
            navigateToSearch(query: query)
        case .unknown:
            navigateToHome()
        }
    }
}

Handling App State

Deep links arrive in different app states:

App Not Running

  • Link data comes through launch options
  • May need to wait for app initialization

App in Background

  • Link arrives via delegate method
  • App state preserved, navigate from current position

App in Foreground

  • Link arrives via delegate method
  • May interrupt current user flow

Consider each scenario:

func handleDeepLink(url: URL, appState: UIApplication.State) {
    let deepLink = DeepLink(url: url)

    switch appState {
    case .active:
        // May want to confirm navigation with user
        showNavigationConfirmation(for: deepLink)
    case .inactive, .background:
        // Navigate directly
        router.route(to: deepLink)
    @unknown default:
        router.route(to: deepLink)
    }
}

Common Challenges

Checklist:

  • SSL certificate valid?
  • AASA/assetlinks.json accessible and valid JSON?
  • No redirects to the file?
  • Domain matches exactly (including www)?
  • App capabilities configured correctly?
  • Testing from external source (not same app)?

Common causes:

  • User previously chose “Open in Safari”
  • AASA file not validated
  • Link domain doesn’t match configuration
  • Testing by typing in Safari address bar (won’t work)

Check:

  • Attribution SDK initialized before checking for links
  • App not killed between install and launch
  • Link created through proper SDK methods
  • Fallback URL configured correctly

Best Practices

URL Design

Design deep link URLs for clarity and flexibility:

Good: https://app.com/product/SKU123
Good: https://app.com/user/johndoe

Less Good: https://app.com/p?id=123&type=product&ref=email

Graceful Degradation

Always have fallbacks:

  • If app not installed → website or app store
  • If content not found → relevant fallback screen
  • If user not authorized → login then redirect

Analytics

Track deep link performance:

  • Link click counts
  • Conversion through links
  • Attribution source
  • Fallback triggers

Security

Validate deep link content:

  • Don’t trust input blindly
  • Validate IDs exist before navigation
  • Consider authentication requirements
  • Sanitize any displayed content

Conclusion

Deep linking transforms how users interact with your app. Proper implementation takes effort, but the user experience improvement is substantial.

Start with universal links for the best user experience, add deferred deep linking for marketing use cases, and build a robust routing system that handles all the edge cases gracefully.