iOS Deep Linking: URL Schemes vs Universal Links

Apr 09, 2026#ios

Deep linking in iOS is the practice of leveraging contextual links to drive a user to specific in-app content. For example, if you tap a link to a song on a website, it can open your music app and play that song directly, instead of opening the website in a browser.

Deep linking can improve user experience, engagement, retention and conversion rates for your app. However, it can also be challenging to implement correctly and consistently across different platforms and devices.

Remember to handle the deep link appropriately within your app once it’s opened. You can use custom URL schemes or Universal Links for deep linking, depending on your requirements and the iOS versions you are targeting.

Custom URL Schemes

Apple supports common schemes associated with system apps, such as mailto, tel, sms, and facetime.

mailto:frank@wwdcdemo.example.com
tel:1-408-555-5555
facetime://user@example.com
sms:1-408-555-1212

In the context of iOS deep linking, both URL schemes and URI schemes are technically the same. The term “URI scheme” is a broader term encompassing various naming conventions used to identify resources on a network, while “URL scheme” specifically refers to the part of a Uniform Resource Locator (URL) that specifies the protocol used to access the resource.

However, in the context of iOS development, “URL scheme” is the commonly used term when referring to deep linking. It defines a custom protocol used to launch your app and potentially navigate to specific content within the app.

  1. First you define the format for your app’s URLs:
myphotoapp://albums/vacation
myphotoapp://albums/vacation?index=1
  1. Then you register your scheme so that the system directs appropriate URLs to your app. Register your scheme in Xcode from the Info tab of your project settings. Update the URL Types section to declare all of the URL schemes your app supports.

  2. And finally you handle the URLs that your app receives. In apps that use the older app delegate lifecycle, implement application(_:open:options:). In scene-based apps, use scene(_:openURLContexts:) instead.

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

  // Determine who sent the URL.
  let sendingAppID = options[.sourceApplication]
  print("source application = \(sendingAppID ?? "Unknown")")

  // Process the URL.
  guard let components = NSURLComponents(url: url, resolvingAgainstBaseURL: true),
    let albumPath = components.path,
    let params = components.queryItems
  else {
    print("Invalid URL or album path missing")
    return false
  }

  // ...
}

Universal Links

Universal Links are supported on iOS 9 and later versions. They allow apps to claim ownership of specific web domains and open specific content within the app when a user taps on a link to that domain.

These are standard HTTP or HTTPS links that can open your app if it is installed, or fall back to your website if it is not. Universal links are more secure and user-friendly than custom URL schemes, and they also support link attribution and measurement.

https://myphotoapp.example.com/albums?albumname=vacation&index=1
https://myphotoapp.example.com/albums?albumname=wedding&index=17

To support universal links in your app:

  1. Set up associated domains to create a two-way association between your app and your website and specify the URLs that your app handles.

Enable the Associated Domains capability in Xcode and add each domain your app handles with the applinks: prefix, such as applinks:myphotoapp.example.com.

Add a JSON file named apple-app-site-association (without an extension) to your website in the .well-known directory. The file must be served over HTTPS. The file’s URL should match the following format: https://your-domain/.well-known/apple-app-site-association.

{
  "applinks": {
    "details": [
      {
        "appIDs": ["TeamID.BundleID"],
        "components": [
          {
            "/": "/path1/*"
          },
          {
            "/": "/path2/deep-link"
          }
        ]
      }
    ]
  }
}

Remember to update the example file with the appropriate App IDs, paths, and your own domain. The app IDs in the file must match your app’s Associated Domains entitlement. Only you can store this file on your server, securing the association of your website and your app.

  1. Handle the incoming user activity when the system opens your app with a universal link. In apps that use the older app delegate lifecycle, respond to an NSUserActivity object with the activityType set to NSUserActivityTypeBrowsingWeb.
func application(
  _ application: UIApplication,
  continue userActivity: NSUserActivity,
  restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
  // Get URL components from the incoming user activity.
  guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
    let incomingURL = userActivity.webpageURL,
    let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true)
  else {
    return false
  }

  // Check for specific URL components that you need.
  // ...
}

Always validate the URL before routing. Treat the host, path, and query values as external input, and reject malformed URLs or links that request sensitive or destructive actions.

Some of the benefits of using universal links are:

  • They are more secure and reliable than custom URL schemes, as they use standard web technologies and prevent URL hijacking.
  • They support link attribution and measurement, as they can pass data parameters to your app and track user engagement.
  • They use regular web URLs, so the same link can fall back to your website on other platforms and devices.
  • They enable continuity features such as Handoff, Shared Web Credentials, and Search.

While custom URL schemes are an acceptable form of deep linking, universal links are strongly recommended.

Open Deep Links in iOS

Communicating between apps using deep links means that one app can launch another app and pass data to it using specially formatted URLs.

  1. Using open(_:options:completionHandler:) in UIKit
if let appURL = URL(string: "https://myphotoapp.example.com/albums?albumname=vacation&index=1") {
  UIApplication.shared.open(appURL, options: [.universalLinksOnly: true]) { success in
    if success {
      print("The URL was delivered to the app successfully.")
    } else {
      print("The URL was not handled as a universal link.")
    }
  }
} else {
  print("Invalid URL specified.")
}
  1. Using openURL action in SwiftUI
struct OpenURLExample: View {
  @Environment(\.openURL) private var openURL

  var body: some View {
    Button {
      if let url = URL(string: "https://www.example.com") {
        openURL(url)
      }
    } label: {
      Label("Get Help", systemImage: "person.fill.questionmark")
    }
  }
}

Handle Deep Links in iOS

  1. In SwiftUI, you can handle Universal Links and custom URL schemes using the onOpenURL modifier. This modifier allows you to specify a closure that will be executed when your app is launched or resumed with a deep link URL.
@main
struct YourApp: App {
  var body: some Scene {
    WindowGroup {
      ContentView()
        .onOpenURL { url in
          // Handle the deep link URL
          handleDeepLink(url)
        }
    }
  }

  func handleDeepLink(_ url: URL) {
    // Handle the deep link URL
    // You can extract any necessary information from the URL 
    // and perform the appropriate actions in your app
    print("Deep link URL: \(url)")

    // Example: Handle a specific deep link scheme and path
    if url.scheme == "your-deep-link-url-scheme" && url.path == "/your-path" {
      // Perform actions specific to this deep link
      // ...
    }
  }
}

UIKit traditionally delivers Universal Links as NSUserActivity objects. SwiftUI passes Universal Links directly as URLs, so use onOpenURL for both Universal Links and custom URL schemes.

  1. In UIKit with UISceneDelegate

If your app isn’t running, the system delivers the URL to the scene(_:willConnectTo:options:) delegate method after launch.

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

  // Get URL components from the incoming user activity.
  guard let userActivity = connectionOptions.userActivities.first,
    userActivity.activityType == NSUserActivityTypeBrowsingWeb,
    let incomingURL = userActivity.webpageURL,
    let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true)
  else {
    return
  }

  // Check for specific URL components that you need.
  // ...
}

Implement the scene(_:openURLContexts:) method, which is called when your app is already running and a deep link URL is being opened.

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
  for context in URLContexts {
    let url = context.url
    // handleDeepLink(url)
  }
}

Debug Universal Links

If Universal Links open in Safari instead of your app, check the following:

  • The app has the Associated Domains capability and includes the exact domain with the applinks: prefix.
  • The domain serves apple-app-site-association at https://your-domain/.well-known/apple-app-site-association.
  • The AASA response returns 200, uses valid JSON, and does not redirect.
  • The AASA file lists the correct TeamID.BundleID and matching path rules.
  • The app has been reinstalled or relaunched after changing entitlements or the AASA file.
  • The test link is opened from outside the same domain in Safari. When browsing a page on the same domain, Safari may continue in the browser instead of opening the app.