Universal Linking

At Arcascope, we implemented universal/deep linking in Shift: The App for Shift Work. The nice people at Arcascope were kind enough to let me publish this doc I wrote about it.

Universal linking allows url links to open an iOS app instead of opening the url in Safari.

Here are a few tutorials: Apple guide, older Apple guide, Kodeco.

Step 1: Create an apple-app-site-association file

  • These are all over the internet: Square, Drizly, Alltrails, Arcascope, Apple
  • There is an aasa validator which can be used to check the validity of your site’s aasa
  • Aasas can be stored in root or in .well-known/ ← see Apple’s
  • While the file cannot have a json suffix, it has to be of type json. To do that set the .htaccess file to..
<Files apple-app-site-association>
ForceType application/json

Step 2: Add the associated domains entitlement to your app

Step 3: Setting the paths

  • If you want the device to only go to your app for some urls, you need to edit the paths in apple-app-site-association file.
  • The paths param specifies which url should open the app.
  • The path must be a valid url. It needs to be visitable by a browser.
  • Test, now you should only go to the app if you enter a url listed in paths.

Step 4: Deep linking

  • By appending a query string to the url specified in paths, the app can retrieve the query string values and send the user to a specific part of the app.
  • The Apple documentation on this is misleading as it only mentions modifying AppDelegate. If your app is in SwiftUI, it listen for events on top-level ContentView.
.onOpenURL {url in
	appState.handleUniversalLink(url: url)
  • Once you have that working, it’s pretty easy to get the query params using URL and URLComponents. E.g. something like…
public func handleUniversalLink (url: URL) {
    let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)
    let queryItems = urlComponents?.queryItems
    guard let tabName = queryItems?.first(where: { $0.name == "tab" })?.value
    tabState.currentState = tabName

In the above case, the url the user would tap would be something like: mydomain.com/thePathSpecifiedInTheAASAFile?tab=home

What about non-Safari browsers?

  • Afaict, universal linking only works for Safari unless you want to spend money for Branch or do a bunch of work with Firebase Dynamic Links. (Here’s a link discussing using dynamic links – I have not tried the suggestions)
  • None of the following apps open the app from Chrome or Firefox (iPhone running iOS16): Resy, Drizly, Amazon, or 1Password.
  • I think the best solution is how Drizly does it, which is to put a link that reads “Open the app” on an html page listed in paths (e.g., myUniversalLinkPage.html). And that link goes to the same page (e.g. myUniversalLinkPage.html). If the user taps that link, the app will open. For whatever reason, that seems to work.

Finally, as part of figuring this out, it seems like Apple may eventually require apple-app-site-association files to be located in /.well-known

Again thanks to Arcascope for letting me publish this.

Leave a Reply