Skip to content
Tolinku
Tolinku
Sign In Start Free
iOS Development · · 10 min read

How to Test Universal Links on Real Devices

By Tolinku Staff
|
Tolinku universal links dashboard screenshot for ios blog posts

Universal Links work beautifully in production, right up until the moment they do not. Then they silently fall through to Safari, and you have no obvious way to know why. The link looked fine. The AASA file looked fine. Your entitlements looked fine. But the app never opened.

This guide is about making that debugging process less painful. You will learn how to verify every layer of the system, test on simulators and physical devices, read the right logs, and avoid the common traps that waste hours of developer time.

Testing a REST API is straightforward: send a request, inspect the response. Testing Universal Links involves multiple systems that do not give you direct feedback.

The Apple CDN fetches and caches your AASA file on Apple's infrastructure, not on your device. The iOS system evaluates link eligibility before your app ever runs. Safari has its own opinion about whether to intercept a tap. Cached state from a previous install can override the current state. All of this happens outside your app's process.

The result is that a bug in your Universal Links setup does not throw an exception. It just quietly opens a browser instead of your app, and you have to figure out which layer failed.

There is also the CDN caching problem. Since iOS 14, Apple's CDN fetches your AASA file and serves it to devices from cache. Changes you make to your AASA can take hours to propagate. If you are testing after an AASA update, you may be testing against a stale cached version even if your server is serving the new file correctly.

This means the testing order matters: validate the server-side setup first, then move to device testing. Do not spend time debugging device behavior when the AASA file itself is malformed. If you need help with the file itself, our AASA file setup guide covers the format, hosting requirements, and common mistakes.

Step 1: Validate Your AASA File

Before touching a device, verify that your AASA file is correctly served and contains valid JSON.

curl -I https://yourdomain.com/.well-known/apple-app-site-association

Check the response headers. The Content-Type should be application/json (or application/pkcs7-mime for signed AASA files). The file must be served over HTTPS. It must not require authentication or redirect. A 301 redirect from HTTP to HTTPS is fine for browsers, but Apple's CDN fetches only the HTTPS URL directly.

Now fetch the content and validate the JSON:

curl https://yourdomain.com/.well-known/apple-app-site-association | python3 -m json.tool

If that command throws a parse error, your AASA file is malformed. Fix the JSON before proceeding. Common mistakes include trailing commas, using single quotes instead of double quotes, and missing the applinks key at the top level.

A valid AASA file looks like this:

{
  "applinks": {
    "details": [
      {
        "appIDs": ["TEAMID.com.yourcompany.yourapp"],
        "components": [
          { "/": "/invite/*" },
          { "/": "/product/*" },
          { "/": "/user/*" }
        ]
      }
    ]
  }
}

The appIDs value is your Team ID concatenated with a period and your app's bundle identifier. You can find your Team ID in the Apple Developer portal under Membership Details. Your bundle identifier is in Xcode under your target's General tab.

Check the Apple CDN Cache

Apple caches your AASA through their own CDN (see our detailed explanation of Apple CDN validation for Universal Links). You can check what Apple has cached right now with this URL pattern:

https://app-site-association.cdn-apple.com/a/v1/yourdomain.com

Replace yourdomain.com with your actual domain. If you see an error or a stale version of your file, the CDN has not picked up your latest changes yet. This can take anywhere from a few hours to about 24 hours after you update the file on your server.

Apple also provides a validation tool for testing AASA file structure. Paste your URL and it will tell you whether the file is reachable and correctly formatted.

Step 2: Testing on the iOS Simulator

The simulator is convenient but has real limitations for Universal Links testing. It cannot install apps from the App Store, and its AASA validation behavior differs from physical devices. That said, it is useful for quick iteration during development.

Use xcrun simctl to open a URL in the simulator:

xcrun simctl openurl booted "https://yourdomain.com/invite/abc123"

The booted argument targets the currently running simulator. If you have multiple simulators running, replace booted with the specific simulator UDID from xcrun simctl list.

For the simulator to open your app instead of Safari, the following conditions must be true:

  • Your app is installed in that simulator
  • The simulator has fetched and recognized the AASA (this happens at app install time)
  • The URL path matches a pattern in your AASA file

If the simulator opens Safari instead of your app, first confirm the URL path matches a component in your AASA. Then reinstall the app: delete it from the simulator, run it again from Xcode, and retry. The act of installing triggers AASA re-evaluation.

Step 3: Testing on a Real Device

Physical device testing is mandatory before shipping. The simulator's link handling does not perfectly replicate device behavior, especially around CDN-cached AASA validation.

The Notes App Method

The most reliable way to trigger a Universal Link on a physical device is through the Notes app. This works because Notes uses UITextView, which processes taps on URLs through the standard iOS link-handling stack.

  1. Open Notes on your device
  2. Create a new note
  3. Type or paste your URL: https://yourdomain.com/invite/abc123
  4. Tap the link

iOS should open your app directly. If it opens Safari, the link is not being recognized as a Universal Link for that device.

One important note: do not long-press the link in Notes. Long-pressing shows a preview sheet with options including "Open in Safari," "Copy," and "Share." A long press does not trigger the Universal Link behavior you want to test. Tap briefly.

Testing via Messages

Paste your URL into an iMessage to yourself and send it. Tap the link in the conversation. This tests the same behavior as receiving a link from another user, which is one of the most common real-world entry points.

SMS links (via the default Messages app with a non-iMessage contact) also trigger Universal Links correctly.

Testing via Safari

Safari has a special behavior: if a user navigates to a URL that matches a Universal Link, Safari will show a banner at the top of the page offering to open the app. But if the user is already on your website and taps an internal link, Safari will not trigger the Universal Link. This is by design. Apple decided that navigating within a website should stay in the browser.

This distinction matters for testing. Navigating from an external app to https://yourdomain.com/invite/abc123 will trigger your app. Clicking that same link while already browsing yourdomain.com in Safari will not.

If you need links within your website to open the app, you have a few options: use a JavaScript redirect to a custom URI scheme as a fallback, use a smart banner, or route through a separate subdomain that is configured as a Universal Link trigger.

Step 4: Reading Console.app Logs

When a Universal Link does not open your app and you cannot figure out why, Console.app on macOS is your best friend. It surfaces system-level logs from your connected device in real time.

  1. Connect your iPhone to your Mac with a cable
  2. Open Console.app (in /Applications/Utilities/)
  3. Select your device from the left sidebar
  4. In the search bar, filter by swcd

The swcd process is the "shared web credentials daemon," which is responsible for AASA fetching and Universal Link validation on iOS. Log entries from swcd will show you exactly what the system is doing: fetching your AASA, caching it, matching (or failing to match) URLs against it. For a deeper look at swcd logs and other diagnostic techniques, see our debugging AASA file guide.

Useful log lines to look for:

  • Fetching app-site-association file confirms the system is trying to retrieve your AASA
  • Failed to fetch indicates a network or server problem
  • JSON parse error means your AASA file has invalid JSON
  • No matching appID means the Team ID or bundle ID in your AASA does not match what is installed

You can also filter for your app's bundle identifier to see logs specific to your app's link handling.

After opening a URL, you should see log entries within a few seconds. If you see nothing from swcd when you tap a link, the system decided early that the link was not a Universal Link candidate, usually because the domain is not in the entitlements list for any installed app.

Step 5: Verify Your App's Entitlements

The Xcode side of Universal Links configuration is an Associated Domains entitlement. Open Xcode, select your target, go to the Signing and Capabilities tab, and look for the Associated Domains section.

You should see entries like:

applinks:yourdomain.com

If that entry is missing, Universal Links will never work regardless of how correct your AASA file is. The entitlement is what tells iOS to even attempt the AASA lookup for your domain.

To verify the entitlement made it into your build, you can inspect the built app:

codesign -d --entitlements :- /path/to/YourApp.app

Look for com.apple.developer.associated-domains in the output. It should list your domain with the applinks: prefix.

For development builds, you can also add the query parameter ?mode=developer to your Associated Domains entries. This tells iOS to fetch your AASA directly from your server rather than Apple's CDN, which is useful when you are iterating on the AASA during development and do not want to wait for CDN cache invalidation.

applinks:yourdomain.com?mode=developer

Remove this before shipping to production.

Automated Testing Strategies

Unit testing Universal Link handling is practical even if you cannot automate the tap itself. The part of your code that receives the URL and decides what to do with it is testable in isolation.

In Swift, your app receives Universal Links through UIApplicationDelegate:

func application(
    _ application: UIApplication,
    continue userActivity: NSUserActivity,
    restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
    guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
          let url = userActivity.webpageURL else {
        return false
    }
    return handleDeepLink(url: url)
}

The handleDeepLink function is what you want to test. Extract it into a separate, testable type. Feed it test URLs and assert it navigates to the correct screen or sets the correct state.

class DeepLinkRouter {
    func route(url: URL) -> DeepLinkDestination? {
        let path = url.path
        if path.hasPrefix("/invite/") {
            let token = String(path.dropFirst("/invite/".count))
            return .invite(token: token)
        }
        return nil
    }
}

This approach gives you fast, reliable tests for the routing logic without needing a device.

For end-to-end testing with XCTest UI tests, you can use XCUIApplication().launch(arguments:) with custom launch arguments that simulate an incoming URL, then assert the correct screen appears.

Common Gotchas

The simulator only works for apps installed via Xcode. You cannot test Universal Links on the simulator using an app installed from TestFlight or the App Store.

Reinstalling the app resets AASA caching on the device. If you update your AASA and want to test the change on a physical device, uninstall and reinstall the app. The device will re-validate the AASA at install time.

In-app browsers do not trigger Universal Links. If a user taps a link inside a WKWebView or an in-app browser (like the one used by many social media apps), Universal Links do not fire. The link stays inside the browser. This is expected behavior, not a bug. If you need Universal Links to work from inside another app's web view, you need to talk to the other app's developers, or route users through a mechanism that exits the in-app browser.

Safari long-press always shows the share sheet. This is not a bug in your setup. On any Universal Link, long-pressing in Safari gives the user options rather than immediately opening the app. Brief taps are what trigger the automatic app open.

Deleted and reinstalled apps may not immediately re-validate. Give the system a few minutes after reinstalling before testing. The AASA validation happens asynchronously after install.

Subdomains need separate AASA entries. If your app handles links from both app.yourdomain.com and yourdomain.com, you need Associated Domains entries for both, and both domains need AASA files. They do not inherit from each other.

Testing Checklist

Use this list before shipping any Universal Links configuration:

  • AASA file returns 200 with Content-Type: application/json
  • AASA JSON is valid (run through python3 -m json.tool)
  • appIDs in AASA matches TEAMID.bundleid exactly
  • Apple CDN at app-site-association.cdn-apple.com/a/v1/yourdomain.com returns the correct file
  • Apple's search validation tool confirms the file is valid
  • Associated Domains entitlement in Xcode includes applinks:yourdomain.com
  • Entitlement is present in the signed build (codesign -d --entitlements)
  • Brief tap on link in Notes app opens the correct app screen
  • Brief tap on link in Messages opens the correct app screen
  • URL paths in the test links match a component in the AASA
  • Console.app shows no AASA fetch errors for the domain
  • Deep link routing logic has unit test coverage
  • App handles the URL gracefully when the linked content does not exist

Putting It Together

Universal Links testing is a process of eliminating variables layer by layer. Start with the AASA file on your server. Confirm the Apple CDN has the right version. Check your entitlements are correct in the build. Then move to the device and use Console.app to watch the system work through its validation steps.

For teams working with a managed deep linking setup, platforms like Tolinku handle AASA hosting and CDN updates automatically, which removes several of the failure points described here. But even with managed hosting, the device-side testing process remains the same: you still need to verify the app receives and routes the URL correctly.

The Tolinku Universal Links documentation covers the server configuration side in detail. For device-specific issues, the iOS troubleshooting guide walks through the most common failure scenarios with step-by-step resolution paths.

For a broader look at Universal Links beyond testing, our complete Universal Links guide covers the full setup, configuration, and troubleshooting process end to end.

Universal Links testing has a reputation for being frustrating, mostly because the feedback loop is slow and the system gives you very little information when something goes wrong. Once you know where to look, the diagnostic process is straightforward. The swcd logs, the Apple CDN check, and a clean reinstall on a physical device will resolve the vast majority of issues.

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.