iOS Deep Linking: Universal Links and Deferred Deep Linking (2026)

iOS Deep Linking: Universal Links and Deferred Deep Linking (2026)

Key takeaways

  • iOS deep linking runs on Universal Links, verified through a two-sided handshake between an apple-app-site-association file on your domain and an Associated Domains entitlement in your app.
  • The handshake happens before your code runs, so most "links don't open my app" bugs are configuration problems, not Swift bugs.
  • Deferred deep linking, routing a new user to the right screen after they install, needs a service layer on top of Universal Links, since Apple's own APIs stop at "app not installed, go to the App Store."

What iOS deep linking actually means

On iOS, "deep linking" covers three related but distinct behaviors: opening the app to a specific screen when it is already installed (a standard deep link), doing that through a real HTTPS URL rather than a custom scheme (a Universal Link), and routing a brand-new user to the right screen after they install the app for the first time (deferred deep linking). Most production apps need all three.

Custom URL schemes like myapp:// still work, but Apple's guidance and modern practice favor Universal Links: standard https:// URLs that open your app directly when installed and fall back to Safari when it is not, with no chooser dialog and no scheme collisions with other apps.

How Universal Links verify: the AASA handshake

Universal Links work through a verification handshake between two files that have to agree. Your app declares which domains it claims via an Associated Domains entitlement. Your web server declares which app may open its links via an apple-app-site-association (AASA) file. If the two do not match, iOS silently drops the claim and the link opens in Safari instead of your app.

The AASA file must be hosted at exactly https://yourdomain.com/.well-known/apple-app-site-association, served over valid HTTPS with no redirects, and returned with a JSON content type. A common failure is a CDN or reverse proxy serving it as text/plain, which causes iOS to ignore it even though a browser can open the file just fine.

{
  "applinks": {
    "details": [
      {
        "appID": "TEAMID.com.yourcompany.yourapp",
        "paths": ["/products/*", "/blog/*"]
      }
    ]
  }
}

The appID field is your Apple Developer Team ID plus your app's bundle identifier. A mismatched Team ID, often from a project that switched developer accounts, is one of the most common reasons verification fails silently. This is the classic AASA format, and it is what Ulinkly serves; on iOS 13 and later Apple also supports an appIDs array with a components block for finer path matching, and both forms are valid.

Configuring the Associated Domains entitlement

On the app side, add the Associated Domains capability in Xcode's Signing and Capabilities tab. With Xcode's automatic signing (the default), this registers the capability on your App ID for you. If you use manual provisioning profiles, also enable it on the App ID in the Apple Developer portal, otherwise the entitlement gets signed as disabled and iOS ignores it.

<key>com.apple.developer.associated-domains</key>
<array>
  <string>applinks:yourdomain.com</string>
</array>

Universal Links do not work in the iOS Simulator, so test on a physical device. The most reliable manual test is emailing yourself the link, opening it from the Mail app, and long-pressing to confirm the context menu offers the option to open it in your app.

Handling the link in your app

Once verification succeeds, iOS delivers the URL to your app through the same continuation-of-user-activity mechanism Handoff uses. In SwiftUI, you handle it with an .onOpenURL modifier on your root view. In UIKit, it arrives through application(_:continue:restorationHandler:) on your AppDelegate or scene delegate.

// SwiftUI
.onOpenURL { url in
    router.handle(url)
}

Keep the handler thin: parse the path and query parameters, then hand off to your existing navigation layer rather than building link-specific routing logic that duplicates it.

Deferred deep linking on iOS

Universal Links only work once the app is installed. For a new user who does not have your app yet, tapping the link sends them to the App Store, and Apple's own frameworks stop there; nothing in iOS automatically tells the freshly installed app which link the user originally clicked. That gap is what deferred deep linking closes.

Solving it requires a service that records the click, then matches the app's first open back to that click after install. Because iOS gives no persistent identifier that survives a trip through the App Store, solutions rely on either a clipboard-based method (the destination is copied to the clipboard before the App Store redirect, and the app reads it back after install) or fingerprint matching (comparing device signals like IP address, screen dimensions, and timezone between the click and the first open within a short time window). Since iOS 14, reading the clipboard shows the user a paste notification, which makes the clipboard method intrusive, so many SDKs — Ulinkly included — use fingerprint matching instead.

Integrating a deep linking SDK

Handling AASA hosting and deferred deep linking matching yourself is workable but adds ongoing maintenance, especially around fingerprint accuracy across iOS versions. A managed SDK hosts the AASA file, runs the matching service, and gives you a listener API. Install Ulinkly's iOS SDK with Swift Package Manager (https://github.com/mohn93/ios_ulink_sdk.git, version 1.2.1 or later) or CocoaPods (pod 'ULinkSDK', '~> 1.2.1'), then initialize it once at startup and observe resolved links:

import ULinkSDK
import Combine

// 1. Initialize once at startup (initialize is async and throwing)
let config = ULinkConfig(apiKey: "YOUR_API_KEY", enableDeepLinkIntegration: true)
Task {
    try await ULink.initialize(config: config)
}

// 2. Forward incoming URLs to the SDK
//    SwiftUI:  .onOpenURL { url in ULink.shared.handleIncomingURL(url) }
//    UIKit:    return ULink.shared.handleIncomingURL(url) from your app/scene delegate

// 3. Observe resolved links with Combine (store cancellables on your object)
ULink.shared.unifiedLinkStream
    .sink { resolved in router.navigate(to: resolved) }
    .store(in: &cancellables)

The full setup, including the dynamic-link stream and cold-start handling, is in the iOS SDK guide.

Frequently asked questions

What is the difference between a deep link and a Universal Link on iOS?

A deep link is any URL that opens your app to a specific screen. A Universal Link is a specific kind of deep link that uses a real HTTPS URL, verified through the AASA handshake, so it opens your app directly with no chooser dialog and falls back gracefully to your website if the app is not installed.

Why isn't my Universal Link opening my app?

The most common causes are a Team ID mismatch in the AASA file, the Associated Domains capability enabled in Xcode but not on the App ID in the Apple Developer portal, the AASA file being served with the wrong content type, or testing in the Simulator, where Universal Links do not work at all.

Does iOS support deferred deep linking natively?

No. iOS routes a user without your app installed to the App Store, but nothing in Apple's frameworks passes the original link data through to the app after install. Deferred deep linking requires a separate matching service, typically using a clipboard method or device fingerprint matching.

Do I need a third-party SDK for iOS deep linking?

Not for basic Universal Links, which you can implement with the AASA file and the Associated Domains entitlement alone. A managed SDK becomes useful once you need deferred deep linking, since building and maintaining accurate cross-install matching yourself is a nontrivial ongoing job.

Want deferred deep linking without building the matching service yourself?

Ulinkly hosts the AASA file and runs the cross-install matching service for iOS. Start free for up to 10,000 monthly active users at ulink.ly/pricing.

Sources

Read More Articles

Explore more guides and insights on deep linking and mobile development.

Back to Blog