Skip to content
Tolinku
Tolinku
Sign In Start Free
Deep Linking · · 6 min read

Deferred Deep Linking on iOS: Implementation Guide

By Tolinku Staff
|
Tolinku deferred deep linking dashboard screenshot for deep linking blog posts

Deferred deep linking on iOS is more constrained than on Android, primarily because Apple controls far more of the stack. The absence of a Play Install Referrer equivalent, the introduction of App Tracking Transparency, and the behavioral differences between SceneDelegate and AppDelegate all shape what is possible.

This guide walks through the complete iOS implementation: detecting a first open, retrieving deferred link data, routing users to the right screen, and handling the cases where attribution is unavailable.

How iOS Deferred Deep Linking Works

When a user taps a deep link on iOS before your app is installed, Safari or the embedding app opens the App Store. The link context (the URL, any campaign parameters, referral codes) cannot ride through the App Store to your app the way Android's Install Referrer does. iOS does not have a native mechanism for this.

Attribution platforms fill this gap through one of three techniques:

  1. Probabilistic fingerprinting. The click captures device signals (IP, OS version, device model, etc.). On first open, your app sends the same signals to the attribution server, which attempts to match them to a recent click within a match window.

    IDFA matching. If the user grants App Tracking Transparency permission, your app can read the IDFA and send it to the attribution server for an exact match against the IDFA captured at click time.

    Clipboard (pasteboard) tokens. When the user taps the link, a short-lived token is written to the clipboard. On first open, your app reads the clipboard and sends the token for an exact match. As of iOS 16, reading the clipboard from an app triggers a system notification. As of iOS 17, it requires explicit user permission in some contexts.

    Tolinku uses all three, in order of accuracy, with configurable fallbacks.

    Prerequisites

    • Xcode 15 or later
    • iOS 15+ deployment target (recommended; iOS 14 minimum)
    • Swift Package Manager or CocoaPods
    • An active Tolinku Appspace with a publishable API key

    Add the Tolinku iOS SDK via Swift Package Manager:

    https://github.com/tolinku/ios-sdk
    

    Or via CocoaPods:

    pod 'TolinkuSDK'
    

    The full SDK reference is at tolinku.com/docs/developer/sdks/ios/.

    Detecting a First Open

    The most important thing to get right: only run deferred link resolution on the true first open of the app. Running it on every launch creates unnecessary network requests and risks routing returning users to onboarding screens they have already seen.

    Store a flag in UserDefaults the first time the app completes attribution resolution:

    import Foundation
    
    struct InstallState {
        private static let key = "tolk_first_open_resolved"
    
        static var hasResolvedFirstOpen: Bool {
            get { UserDefaults.standard.bool(forKey: key) }
            set { UserDefaults.standard.set(newValue, forKey: key) }
        }
    }
    

    Check this flag before triggering any resolution logic.

    SceneDelegate Integration

    Tolinku dashboard showing route configuration for deep links

    Apps using UISceneDelegate (the default for iOS 13+) should initialize Tolinku and run first-open logic in scene(_:willConnectTo:options:):

    import UIKit
    import TolinkuSDK
    
    class SceneDelegate: UIResponder, UIWindowSceneDelegate {
        var window: UIWindow?
    
        func scene(
            _ scene: UIScene,
            willConnectTo session: UISceneSession,
            options connectionOptions: UIScene.ConnectionOptions
        ) {
            guard let windowScene = scene as? UIWindowScene else { return }
            window = UIWindow(windowScene: windowScene)
    
            Tolinku.configure(publishableKey: "tolk_pub_your_key_here")
    
            if !InstallState.hasResolvedFirstOpen {
                resolveFirstOpen()
            } else {
                showMainInterface()
            }
        }
    
        private func resolveFirstOpen() {
            Tolinku.shared.resolveDeferred { result in
                DispatchQueue.main.async {
                    InstallState.hasResolvedFirstOpen = true
                    switch result {
                    case .success(let deepLink):
                        self.route(to: deepLink)
                    case .failure:
                        self.showMainInterface()
                    }
                }
            }
        }
    
        private func route(to deepLink: TolinkuDeepLink) {
            // deepLink.path, deepLink.params, deepLink.referralCode, etc.
            // Route to the appropriate screen based on the link data
            showMainInterface(initialRoute: deepLink)
        }
    
        private func showMainInterface(initialRoute: TolinkuDeepLink? = nil) {
            let vc = MainViewController(deepLink: initialRoute)
            window?.rootViewController = UINavigationController(rootViewController: vc)
            window?.makeKeyAndVisible()
        }
    }
    

    The resolveDeferred call handles fingerprinting, IDFA lookup (if available), and clipboard reading internally. It returns either a TolinkuDeepLink with the routing data or an error if no matching click was found.

    Handling AppDelegate-Only Apps

    If your app still uses AppDelegate without scenes (less common but still valid), run resolution in application(_:didFinishLaunchingWithOptions:):

    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        Tolinku.configure(publishableKey: "tolk_pub_your_key_here")
    
        if !InstallState.hasResolvedFirstOpen {
            Tolinku.shared.resolveDeferred { result in
                DispatchQueue.main.async {
                    InstallState.hasResolvedFirstOpen = true
                    // Handle routing
                }
            }
        }
        return true
    }
    

    ATT and IDFA Access

    Requesting ATT permission is optional but increases attribution accuracy. If you choose to request it, do so at a moment that makes sense for your app's flow, not immediately on launch (Apple's guidelines recommend providing context before the system prompt appears).

    Add NSUserTrackingUsageDescription to your Info.plist:

    <key>NSUserTrackingUsageDescription</key>
    <string>We use this to attribute your install and personalize your experience.</string>
    

    Then request permission before calling resolveDeferred:

    import AppTrackingTransparency
    import AdSupport
    
    func requestTrackingAndResolve() {
        if #available(iOS 14, *) {
            ATTrackingManager.requestTrackingAuthorization { status in
                // Proceed with resolution regardless of status.
                // Tolinku will use IDFA if granted, fingerprinting otherwise.
                Tolinku.shared.resolveDeferred { result in
                    DispatchQueue.main.async {
                        self.handleDeferredResult(result)
                    }
                }
            }
        } else {
            Tolinku.shared.resolveDeferred { result in
                DispatchQueue.main.async {
                    self.handleDeferredResult(result)
                }
            }
        }
    }
    

    Always call resolveDeferred inside the ATT completion handler, not before it. If you call resolution before the ATT prompt resolves, the SDK cannot read the IDFA and will fall back to fingerprinting even if the user intends to grant permission.

    Clipboard / Pasteboard Matching

    Tolinku's SDK handles clipboard matching internally when fingerprinting is the only available option. On iOS 16+, reading the clipboard from an app causes the system to display a brief notification at the top of the screen indicating that your app read from the clipboard. This is not a blocking permission prompt, but users do see it.

    If this creates a bad experience for your app, you can disable pasteboard matching in the SDK configuration:

    var config = TolinkuConfiguration(publishableKey: "tolk_pub_your_key_here")
    config.pasteboardMatchingEnabled = false
    Tolinku.configure(with: config)
    

    Disabling it means fingerprinting is the only fallback when ATT is not granted. Attribution accuracy will decrease, but the clipboard notification disappears.

    SKAdNetwork Interaction

    SKAdNetwork is Apple's privacy-preserving install attribution framework. It tells ad networks which campaigns drove installs, with aggregated and delayed reporting. It does not support deferred deep linking.

    If you are running paid UA campaigns, register your SKAdNetwork IDs in Info.plist per the Apple documentation. This is separate from your Tolinku integration. Tolinku handles the deep link routing; SKAdNetwork handles aggregated conversion reporting to ad networks.

    You can configure a SKAdNetwork conversion value update in parallel with Tolinku's first-open resolution if you need to report on conversion values for paid channels.

    Testing Your Implementation

    Testing deferred deep linking on iOS requires actual devices. The iOS Simulator does not replicate the App Store install flow.

    A practical testing workflow:

    1. Generate a test link from your Tolinku dashboard.
    2. On a physical iOS device that does not have your app installed, tap the link. Confirm it redirects to the App Store.
    3. Install the app via TestFlight or a direct install (Xcode device run works for development).
    4. Open the app. Verify that resolveDeferred is called and that the callback receives the expected deep link data.
    5. Close and reopen the app. Verify that the deferred resolution does NOT run again (the hasResolvedFirstOpen guard should prevent it).

    If you have issues, enable verbose logging:

    Tolinku.setLogLevel(.debug)
    

    This prints resolution steps to the console, showing which matching method was used and what data was returned.

    Routing After Resolution

    The TolinkuDeepLink object contains:

    • path: the original link path (e.g., /referral/ABC123)
    • params: a dictionary of URL parameters
    • referralCode: convenience accessor if a referral code is present
    • campaignId, source, medium: attribution data if available

    Use these to drive your routing logic. For complex apps with many possible destinations, a central routing coordinator or a DeepLinkRouter class handles this cleanly:

    class DeepLinkRouter {
        static func route(deepLink: TolinkuDeepLink, from nav: UINavigationController) {
            switch deepLink.path {
            case _ where deepLink.path.hasPrefix("/referral"):
                let vc = ReferralOnboardingViewController(code: deepLink.referralCode)
                nav.setViewControllers([vc], animated: false)
            case _ where deepLink.path.hasPrefix("/promo"):
                let vc = PromoViewController(params: deepLink.params)
                nav.setViewControllers([vc], animated: false)
            default:
                nav.setViewControllers([MainViewController()], animated: false)
            }
        }
    }
    

    For more on building personalized onboarding flows using deferred link data, see the deferred deep linking for onboarding guide.

    Further Reading

Get deep linking tips in your inbox

One email per week. No spam.

Ready to add deep linking to your app?

Set up Universal Links, App Links, deferred deep linking, and analytics in minutes. Free to start.