A Deep Dive Into iOS Deep Linking

Ross Freeman
WW Tech Blog
Published in
6 min readFeb 11, 2020

Deep linking is a powerful way to allow users to interact with your apps. From a simple tap, users can be brought from Safari, Mail, Notes, and other apps right into your app’s content. While this interaction may seem simple on the surface, there is actually a lot going on under the hood that needs to be understood before implementing this feature in your app.

Types of Deep Links

iOS provides support for 2 different types of deep links: URL schemes and universal links. URL schemes allow you to create your own scheme that only corresponds to actions in your app. For example, you can have something like myapp://settings to bring users to the settings page in your app. The big advantage here is that these custom schemes can only be used to open your app, superseding any OS logic for determining if your app should open. However, this is also its biggest disadvantage. If someone doesn’t have your app installed and taps on one of your URL schemes, iOS will display a nasty error saying that it can’t open the URL. There’s no indication that they should download your app to continue. This creates a suboptimal experience for potential would-be users who encountered your link. Additionally, URL schemes are highlighted by Apple as being potentially susceptible to security concerns, so they highly recommend avoiding them, if possible.

Universal links look just like any regular URL that you know and are used to. For example, it could look something like https://cmx.weightwatchers.com/nui/my-day. The advantage here is that since this is just a standard URL, iOS will open Safari and navigate to that page if the user doesn’t have your app installed. This is a far better UX than a non-descriptive error message.

The rest of this article will focus exclusively on universal links, since they are recommended more than URL schemes and contain some complexities that are worth explaining.

How Does iOS Know to Open Your App?

Before diving into the actual setup process, it’s important to go over how iOS determines to open your app instead of Safari when a universal link is tapped. There is a lot of complexity going on here to provide different experiences, depending on your situation.

When a user installs your app, iOS checks the app’s manifest for its associated domains (provided by you in Xcode) and specifically checks for domains beginning with applinks:, since these indicate a domain for deep linking. iOS will then take each matching domain and hit the /.well-known/apple-app-site-association path for those domains. This path should contain a specifically structured JSON file provided by you, which is explained more in-depth later. The JSON file contains information about which paths for that domain are valid deep links. iOS will store these valid paths in a local manifest containing all valid deep links for all installed apps. Note that this process only happens in two instances: when your app is first installed and for each subsequent update. If you decide to update the valid paths, you will need to release a new update to force iOS to re-check that JSON file.

When any link is tapped from a valid context, iOS will first check its local deep link manifest to see if it matches any known deep links. If a match is found, the URL is routed to that app to handle. Otherwise, the link opens normally in Safari. This process happens quickly behind the scenes and is totally hidden from the user.

What is a Valid Context?

In my description earlier, I mentioned that iOS will only check its local manifest if a link is tapped in a valid context. So what is a valid context? iOS has logic that determines if a tapped link should be treated as a potential deep link or a regular link. This logic is in place to provide a better UX and use the behavior that makes the most sense to the user.

iOS will consider a URL as being a potential deep link if it is opened from any non-Safari app. For example, tapping on a link in an email, text message, or note in the Notes app will cause iOS to consider it for deep linking. On the other hand, if a URL is pasted into the navigation bar in Safari, iOS will not consider it for deep linking and just navigate to that page. The only time when iOS will consider a URL for deep linking from Safari is if a user navigates to a different domain by tapping on a link on the current page. For example, if a user is searching Google and taps on a link to https://weightwatchers.com, iOS will consider that for deep linking since the domain, weightwatchers.com, is different from the original domain, google.com. Note that this may not work for redirects.

Preparing Your App for Deep Linking

Now that we have covered how deep links work under the hood, it’s time to actually implement deep linking!

The first thing you’ll want to do is determine which domains should be considered for deep linking. This may just be your website’s standard domain or subdomain, whichever you prefer. Once you have your list of valid domains, you will need to add them to Associated Domains under Signing & Capabilities in your Xcode target. You need to prefix all domains with applink: to indicate that this associate domain is meant for deep linking.

The next step is to prepare your Apple App Site Association file, or AASA. As discussed earlier, this is an extremely important file for you to host and is vital to making sure your deep link works properly. Your AASA should look something like this:

Simply replace the appID value with your app’s bundle ID, prefixed with your team ID. The paths array should also contain a list of all valid (and if necessary, invalid) paths.

You will need to host this file at the /.well-known/apple-app-site-association path for each one of your valid domains. Note that even though this is technically a JSON file, the path doesn’t contain the json suffix.

Handling Deep Links

Now that your app is set up for deep linking, you need to actually handle the deep link. When your app is opened due to a universal link, it will trigger the application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool method in your AppDelegate. The first thing you should do is verify that there is in fact a deep link to handle. You should first check that userActivity.activityType is NSUserActivityTypeBrowsingWeb to make sure that a universal link triggered this method. Then, make sure userActivity.webpageUrl exists, since this would be the URL that you need to handle.

Now that you have access to the URL, you will have to write logic to correctly parse and handle it. While there are a multitude of ways to go about it, one of the cleaner and more efficient ways is to simply use a path tree structure. At WW, we implemented it like this:

This handler provides a few conveniences for us. It provides support for wildcard * characters in URLs, implements logic for reserving certain URLs for valid users only, and has proper error handling. An interesting detail to point out here is the lack of URL host checking. While there was much debate about this, we decided to omit this since we support a number of different domains across different markets and development environments. Checking against every valid host is an additional point of potential failure and we felt that it wasn’t worth handling. The associated domain file already limits the valid domains, so in a way that does the valid domain checking for us. We do, however, still check the URL’s scheme to provide support for URL schemes in addition to universal links.

With this structure, we can then simply just call the handleLink method.

The canDeepLink variable simply indicates if the user is a valid subscriber so that the handler can determine if the URL should be handled. If not, it throws the subscriberRequired error and stores the user activity object for handling later. When the user logs in, the pending user activity is evaluated and we will attempt to handle the URL again.

In order to actually register a deep link to handle, we can place the following code anywhere in our app:

This makes our code clean, simple, and effective. From here, it is just a matter of architecting your app to be able to handle navigation properly. (I won’t cover that here, since it warrants a dedicated article to cover that topic.)

While your actual deep linking needs may be different from ours at WW, hopefully the information provided here is a good baseline for getting started with your own deep linking implementation. Happy deep linking!

— Ross Freeman, iOS Engineer at WW (formerly Weight Watchers)

Interested in joining the WW team? Check out the careers page to view technology job listings as well as open positions on other teams.

--

--