Deep Linking in React Native with Universal Links and URL Schemes

How to implement deep linking technologies for iOS React Native apps

Universal Links and URL Schemes are two iOS features that allow developers to configure links that jump straight into an app at a particular screen, no matter how deep into the app. This article outlines the differences between these two features, and how to integrate them within Xcode and React Native projects, including ejected Expo projects, covering use cases between the two deep linking methods. Useful React Native packages to facilitate the integrations will also be documented.

A Brief History of Deep Linking in iOS

Deep linking is quite a generic term that encompasses the idea of navigating to a specific indexed piece of material, whether that be a webpage or a particular place in an application. The concept has been practiced since the inception of HTTP and the hyperlink, and has proved to be an integral part of navigating websites in the browser.

In more recent times, the concept has been adopted for use with apps as their complexity and capabilities have grown. The ability to jump to a specific state in an application is particularly useful for iOS apps, that exist in an allotment of sandboxed resources, AKA, Apple’s “walled garden” approach to apps. Apple therefore adopted Deep Linking as an answer to jumping into an app while upholding these security mechanisms.

Deep linking was bought to iOS very early on

Deep linking for iOS firstly came in the form of URL Schemes, which is what the term Deep Linking actually refers to in iOS. mailto://<address> is a common URL Scheme that jumps into the default mail application of the device in question. facetime://<email> and facetime-audio://<email> are other examples specific to iOS. URL Schemes have in fact been around in iOS since 2008, and have played a key role in navigating between apps.

Check out Apple’s documentation archive on URL Schemes to see all the schemes they have implemented. Also, checkout the supported URL Schemes in React Native.

What is more relevant for developers though is the ability to define Custom URL Schemes, that link to their app specifically. This is done in Xcode through a streamlined GUI, so you could jump to your app with its specific name, such as the myappname:// URL Scheme. From here, your URL Scheme can be expanded to point to a particular place in your app:

# URL Schemes can simply navigate to a particular screen
myapp://settings
# or a "deeper" state of the app
myapp://blog/latest/page/5

These links work within iOS apps such as Notes, as well as in the browser.

But where does React Native fit into this solution? The answer lies within the Linking API, a built-in link to URL Schemes that can be subscribed to via an event listener, that monitors incoming URL events. This event listener can then handle that event accordingly, such as jumping to a particular screen via react-navigation or other routing means. We’ll visit exactly how this is done further down.

URL Schemes were the first foray into deep linking for iOS apps. Universal Links came later, that link an actual domain name to an app.

Universal Links use a website URL scheme to deep link an app

Universal Links were introduced in 2015 with iOS 9 as a superior means of deep linking into an app. By linking a domain name (that the developer or app company must own) to the app, and using its URL Scheme to navigate to specific points of an app. By doing this, the website and app are in sync in terms of their content delivery.

In order to link a domain to an app, some Xcode configuration is needed, as well as an appdelegate.m method to handle incoming URL events. Not only this, but a JSON file called the Apple App Site Association file, must be configured and hosted on the supported domain name in question, at https://mydomain/.well-known/apple-app-site-association. Within this file, we can configure specific URL patterns to deep link into the app — you do not need to support the entirety of domain patterns for the web version, even though you could with a wildcard (more on this later).

This may seem a complex solution but is quite straight forward in actual practice, especially when Xcode’s automatic provisioning features are used. We’ll also cover exactly how to integrate Universal links further down the article.

URL Schemes and Universal Links can be used together, although Apple recommends Universal Links should be used as the standard means of deep linking. This advice is warranted if you indeed have a website counterpart of your app, whereby your app contains a complex URL scheme that is also used for other versions of the app — such as an Android counterpart. If you wish to prompt a user to jump into the app at certain parts of your website, then Universal Links is also the preferred deep linking solution.

Universal Links will prompt the user to open the app via a sub-header, that you may have noticed in iOS Safari, where you have the corresponding app installed. Here is an example for my Chinese grammar revision app:

An Open App prompt will appear with web pages configured with Universal Links.

If a Universal Link is tapped in a Native app on a device the app is installed on, it will take the user directly into the app.

If your app on the other hand is iOS exclusive and only requires a simple deep linking solution, such as to the top-level screens of your app (perhaps navigating between tabs within a Tab Bar Controller) then a URL Scheme will be the solution that makes the most sense.

With that said, hopefully by the end of this article the reader will have a clear idea of which of the technologies to adopt. Let’s firstly explore integrating URL Schemes, before moving onto Universal Links.

Setting up a URL Scheme

This section documents the setup steps of implementing a URL Scheme for your app.

The first step is to define the scheme name itself; this is done in Xcode. With your project selected within the Project Navigator, head over to the Info tab and scroll down to URL Types. It is here that URL schemes are defined:

  • For the Identifier field, it is common to enter your app’s bundle ID (in the com.domain.app format).
  • In the URL Schemes field, put a name for the scheme itself that act as the URL prefix.
  • Leave the role as Editor, as your app is responsible for defining this URL Scheme. If you are adopting a URL Scheme from another app, change this value to Viewer.

Here is a generic URL Type that has been pre-filled in Xcode:

A URL Type defined in Xcode.

Official Apple documentation on registering URL Schemes can be found here.

Now within your appDelegate.m file, include the following method to handle incoming URLs, linking toRCTLinkingManager supplied by React Native:

#import <React/RCTLinkingManager.h>// ...#pragma mark - Handling URLs- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url
sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
{
return [RCTLinkingManager
application:application openURL:url
sourceApplication:sourceApplication
annotation:annotation
];
}
// ...

If your project is flagging a duplicate method, be sure to comment out the pre-existing one, to ensure that RCTLinkingManager is being returned.

Expo projects in development use the exp:// URL Scheme

It is important to note at this time that when your app is running in Expo’s development environment, E.g. before publishing and building your app build for TestFlight, the exp:// URL Scheme is used; we’ll prove this is the case in the next section when we monitor incoming URL events in React Native.

For this reason, it may be beneficial to support multiple URL Schemes when monitoring URL events in a development environment — and this is exactly what will be documented in the next section, with a helper package available on NPM.

Changing an Expo project’s URL Scheme

If you are coming from an ejected Expo project, you may have noticed that a URL Scheme already existed for your app — this was an automatically generated string that Expo configured for you. It is most likely that you do not wish to use this URL Scheme, and would like a more readable and friendly alternative.

The full instructions — including how to set a different scheme — have been documented in Expo’s official documentation. On the iOS side at least, the process of swapping the URL Scheme is quite straight forward:

  • Change the scheme field in app.json to your new scheme name.
  • Change the value of the first ofCFBundleURLSchemes occurrence in your plist file, at ios/<your-project-name>/Supporting/Info.plist.

After this, feel free to remove the previous URL Type within Xcode. To remove a URL Type, navigate to your info.plist file and find the URL types key, and press the minus (-) icon of the corresponding URL type.

Now, if you build your project and install it from TestFlight, you will be able to test your URL Scheme in action — without any configuration on the React Native side. The easiest way to do so is to just type a URL in the Notes app. Clicking that URL should immediately bring your app into the foreground, in its current state.

# testing a URL with your URL scheme<url_scheme>://testing

Before moving onto configuring Universal Links, let’s cover how to monitor these URL events in React Native, that will ultimately give your app the means to navigate to the desired screen, based on that incoming URL.

Both URL Schemes and Universal Links are monitored using React Native’s Linking API. The only difference will be the URL itself.

Monitoring URL Events in React Native

As already ascertained, React Native’s Linking API gives us access to deep linked URL events. To facilitate the monitoring of these events, the react-native-deep-linking package can be used. As the only dependency to get this working, install it with yarn:

yarn add react-native-deep-linking

react-native-deep-linking gives a level of abstraction to configure and manage incoming URL events, saving time and additional un-needed complexity.

We’ll be using 3 methods from the imported DeepLinking object:

  • DeepLinking.addScheme to store each of the URL Schemes your app supports.
  • DeepLinking.evaluateUrl(url) to pass a URL into the DeepLinking route controller.
  • DeepLinking.route(route, response => {}) to handle each route we expect from the URL.

These APIs can be used alongside Linking and useEffect so functional components can handle these incoming URLs. As our app might be supporting multiple URL Schemes, such as exp:// in an Expo development environment, it is wise to define these schemes as global constants:

// constants.jsexport const URL_SCHEMES = [
'exp://',
'myapp://',
];

Now these can be imported into the component(s) where URL events will be monitored. Here is a solution that looks out for one route, /settings, and navigates to the Setting screen if it is matched. In addition, the Linking event listener is managed through useEffect:

...
import { URL_SCHEMES } from './constants'
import DeepLinking from 'react-native-deep-linking'
export const App = (props) => { // add URL schemes to `DeepLinking`
for (let scheme of URL_SCHEMES) {
DeepLinking.addScheme(scheme);
}
// configure a route, in this case, a simple Settings route
DeepLinking.addRoute('/settings', (response) => {
navigation.navigate("Settings");
});
// manage Linking event listener with useEffect
useEffect(() => {
Linking.addEventListener('url', handleOpenURL);
return (() => {
Linking.removeEventListener('url', handleOpenURL);
})
}, []);
// evaluate every incoming URL
const handleOpenURL = (event) => {
DeepLinking.evaluateUrl(event.url);
}
return(
<Text>App</Text>
);
}

Using the useEffect hook alongside Linking and DeepLinking objects, we can construct the event handling logic with relative ease.

Now with a means of monitoring incoming URLs, let’s turn our attention to setting up Universal Links.

Setting up Universal Links

Unlike what some articles would lead you to believe, Universal Links configuration is no more complex than URL Schemes, with the only additional complexity being a JSON file that must reside on your server hosted by the domain you are supporting. In addition to this, a bit of Xcode configuration is required.

Universal Links App Delegate method

An additional appDelegate.m method is required to get Universal Links working.

Jump back to Xcode and copy the following below the previous URL Scheme method, in the #pragma mark — Handling URLs section, that also returns the RCTLinkingManager, routing URL Events to React Native’s Linking API once again:

// Universal Links- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity
restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler
{
return [RCTLinkingManager
application:application
continueUserActivity:userActivity
restorationHandler:restorationHandler
];
}

Adding Associated Domains Entitlement

Within your Signing & Capabilities section of Xcode, add the Associated Domains entitlement, thus giving your app the capability to support external domains for not only Universal Links, but web credentials too (this is for another piece!).

Within the top empty field that presents itself after adding this entitlement, insert applinks: followed by your domain name. Here is the configuration for an app I work with, that supports the chinesegrammarapp.com domain:

Supporting Universal Links with the Associated Domains entitlement and applinks: domain prefix

The applinks: prefix is a nod to Xcode that this domain is being used for Universal Linking, and is one of a few prefixes that Xcode will recognise depending on the technology you are implementing. webcredentials: being another example.

If you now hop into your Provisioning Profiles in your Apple Developer account, Associated Domains should now be enabled for your app. This is an optional check, but verified the Associated Domains entitlement has been added server-side:

Associated Domains enabled within App Provisioning

Adding the AASA (Apple App Site Association) File

The last piece of setting up Universal Links is adding a JSON file to your server that will be served via your supported domain. Apple provides extensive documentation on setting this up, but the structure is relatively straight forward.

At the time of writing, the iOS 13 structure of the AASA file, found here, does not validate on my attempts to support it, and is not recognised as a Universal Links configuration. Therefore, I recommend adhering to the iOS 12 and below format for the file. This is indeed the format documented throughout Apple’s documentation. This can be a confusing conflict, especially those like myself who aim to support the latest standards. For now though, the iOS 12 and below format works in iOS 13 too.

With the above in mind, here is a AASA file that supports a link to settings, as well a product page, that will take an arbitrary value via the wildcard character:

{
"applinks": {
"apps": [],
"details": [{
"appID": "<TeamID>.com.example.myapp",
"paths": ["/settings", "product/*]
}
]
}
}

There are only a couple of things to take note of here:

  • The appID field expects your Team ID (found here on your Membership page) to prefix your App Bundle ID.
  • Each URL is configured in the details array, with each item in that array being an object with the appID, and a paths array of each URI you are supporting. The wildcard (*) character can be used for supporting unique identifiers within the URI.
  • The apps field must be left as an empty array.

Now simply add the file within a .well-known folder on the top level of your domain, so the file will be hosted at:

https://your-domain.com/.well-known/apple-app-site-association

Note that you must be serving over HTTPS, with no redirects. Also, do not append .json at the end of the filename. From here, re-install your app to fetch the AASA config. This will store the config as metadata on device.

React to App Launches from Universal Links

Back to React Native, what we can now check with Linking is whether a universal link triggered the app launch.

This is done with Linking.getInitialURL() API, and can be called in useEffect upon an initial component render. The initial URL, if it exists, can be treated like any other incoming URL. The standard way to check for initial links is via AppState:

// checking if an initialURL is presentconst [initialised, setInitialised] = useState(false);useEffect(() => {
AppState.addEventListener('change', handleAppStateChange);
if (Linking.getInitialURL() !== null) {
AppState.removeEventListener('change', handleAppStateChange);
}
}, []);
const handleAppStateChange = async (event) => {
const initial = await Linking.getInitialURL();

if (initial !== null && !initialised) {
setInitialised(true);
// app was opened by a Universal Link
// custom setup dependant on URL...
}
}

Note that an initialised useState has also been used to prevent repeatedly doing custom setup every time the app falls into the foreground. With your AppState event listeners handling initialURL(), Universal Links can now be tested in both in the browser and in native apps.

Linking also contains methods for opening URLs and opening the app’s settings, with openURL() and openSettings() methods respectively.

Another Expo URL Gotcha: exps://

If you are validating URLs, be aware that Expo will will reformat the initial URL with the exps:// prefix in development mode, replacing https://. This can be verified by logging your initial URL in your AppState event handler:

const handleAppStateChange = async (event) => {
const initial = await Linking.getInitialURL();
console.log(initial);
}

Not a major issue, but may lead to errors in your code.

Troubleshooting Universal Links

Overall, Universal Links are not too troublesome to implement, although there are times where you could be left scratching your head if they do not appear to be working.

Before assuming there is an error with your configuration, consider the following important points:

  • It can take up to 48 hours for your Apple App Site Association (that will be referred to as AASA from now on) file to update on the device, during which time the most up to date URLs will not work.
  • Upon updating Universal Links, delete your app and re-install it. It is at install time that your app fetches the AASA configuration and stores it as app metadata on device.
  • To verify a URL is working, copy it into the Notes app, and long press it to open the context menu. The top link should be Open in <App Display Name> . In addition to this, tapping the link should open your app directly.

App Search API Validation Tool

The Apple hosted App Search API Validation Tool is be far the simplest means of validating an AASA file, among other metadata a your web pages contain. Just be sure to point to a URL that is configured within your AASA file, and not to a URL that is not configured.

The App Search API Validation Tool always picks up the live version of your AASA, so you need not worry about it fetching cached versions.

Sysdiagnose

If you are experiencing issues you cannot fathom, you could turn to more verbose debugging with the sysdiagnose tool for iOS. This tool will be able to log exactly what AASA configuration is currently sitting on your device.

Other issues

At the time of writing, there is an open discussion on the Apple Developer forums with developers having issues with Universal Links. If you are having difficulty getting them working, check out this thread to see if you’re experiencing the same issues.

Saying this, I have not experienced issues integrating Universal Links — yet!

Summary

This article has aimed to document the implementation of Deep Linking mechanisms for iOS, pertaining to URL Schemes and Universal Links. We’ve covered the benefits of both technologies and where they make sense to be used. We then covered how to implement both the technologies, and how to pick up URL events in React Native.

React Native is quite flexible with how you handle incoming URL events; you can react to them within components just like you would to fetch responses, or a live feed. In addition to this, you can also pick up an “initial route” via the Linking API, that checks whether the app was opened via a URL Scheme or Universal Link. If this was the case, you could display a custom landing screen or navigate to a different default screen.

Deep Linking has become an integral component for apps with complex hierarchies, but also for streamlining features that require jumping out and back into the app, such as password resetting, email verification, and so forth. Nonetheless, the reader should now know which Deep Linking mechanism is right for their app, and how to implement them.

Programmer and Author. Director @ JKRBInvestments.com. Creator of LearnChineseGrammar.com for iOS.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store