Deeplinks on iOS

Deeplinks on iOS as we have implemented on Farechoice

iOS has 2 main ways to handle redirects: URL Schemes and Universal Links. In this page deep linking refers to the latter.

The Deeplinks for the iOS app are used to open the Farechoice app from sms or email received after a booking confirmation OR from going to www.google.com and searching for Farechoice and clicking the first link https://farechoice.taxi. This works for Safari, but also for other browsers like Google Chrome.

There are several steps involved in implementing universal links.

1. Configure the app to register approved domains on the Apple developer account

First in to the developer account, under the Identifiers section, select the bundle id.

Under Capabilities scroll down to Associated Domains and enable it. You'll need to regenerate the existing provisioning profiles also to add this capability.

2. Configure the app to register approved domains in the Xcode project

Back in the Xcode project select the target, Signing & Capabilities and scroll down to Associated Domains. Add your domain in the format shown in the example below.

To break this down a bit:

  • applinks: is used to indicate this is indeed for the Universal Link declaration.
  • farechoice.taxi is the domain that hosts our apple-app-site-association json. Apple doc says to limit this list to no more than about 20 to 30 domains.

After completing this step, make sure that the entitlements file has been added to the project. Xcode should take care of this automatically.

3. Configure the website to host the ‘apple-app-site-association’ file (AASA for short)

This file is meant to prove ownership of the domain. We've added ours to [<https://farechoice.taxi/.well-known/apple-app-site-association>].

🚧

Although it is a JSON, DO NOT add a .json extension to the file.

There are some useful validators online to help set up your AASA file, like: https://branch.io/resources/aasa-validator/

Let's have a look at the structure of this file

{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": {TEAM_ID}.{BUNDLE_ID}",
        "paths": [
          "*"
        ]
      }
    ]
  }
}
  • Applinks indicate this is indeed for the Universal Link declaration.
  • Apps should be left as an empty array (quote from Apple: “The apps’s key in an apple-app-site-association file must be present and its value must be an empty array).
  • Details contains an array of the apps and the mapping of each subpath to the respective app.
    • The appID is made up of the team ID (found on the developer account) and the bundle ID.
    • paths is an array of strings that specify which paths are included or excluded from association.

As we only have one app, any subpath will lead here, hence the *.

You can use * as a wildcard to enable all paths in a directory (apple doc says: Use * to specify your entire website) and ? to match a single character (/archives/201?/ for example). Please note that these strings are case sensitive and that query strings and fragment identifiers are ignored.

If you want to exclude a subpath, just add NOT at the beginning. The system evaluates the path in order and will stop when finding a NOT, so take that into consideration when selecting the order.

Here's an example of a more complex AASA file: [<https://www.reddit.com/apple-app-site-association>]

iOS will only attempt to fetch the AASA file over a secure connection (HTTPS). So, while hosting the AASA file, please ensure that the AASA file:

  • Is served over HTTPS.
  • Uses application/json MIME type.
  • Don’t append .json to the apple-app-site-association filename.
  • Has a size not exceeding 128 Kb (requirement in iOS 9.3.1 onwards).

4. Handle the redirect in AppDelegate

One more step to go. Back in the Xcode project, in AppDelegate we implemented the following method:

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

  print("URL Components: \(components.path)")

  let handledInApp = myPrivateMethodThatValidatesIfThisIsForFarechoice(url: url)
  return handledInApp
}

After all of the setup above (if done properly), when the user clicks on a farechoice.taxi link, this method will be fired.

The purpose of this method is to tell iOS if this is the app that should pick up the redirect or not. If it's not and no other match is found, then it will default to the webpage. Basically you need to return true if the app wants to capture it and false if not.

Use components.path to see any custom paths that need handling in the app (like going to a specific screen for example)

🚧

Warning from the Apple docs:

Universal links offer a potential attack vector into your app, so make sure to validate all URL parameters and discard any malformed URLs. In addition, limit the available actions to those that don’t risk the user’s data. For example, don’t allow universal links to directly delete content or access sensitive information about the user. When testing your URL-handling code, make sure your test cases include improperly formatted URLs.

Things to keep in mind *source:

  • When a user taps a universal link that you handle, iOS also examines the user’s recent choices to determine whether to open your app or your website. For example, a user who has tapped a universal link to open your app can later choose to open your website in Safari by tapping a breadcrumb button in the status bar. After the user makes this choice, iOS continues to open your website in Safari until the user chooses to open your app by tapping OPEN in the Smart App Banner on the webpage.
  • If you instantiate a SFSafariViewController, WKWebView, or UIWebView object to handle a universal link, iOS opens your website in Safari instead of opening your app. However, if the user taps a universal link from within an embedded SFSafariViewController, WKWebView, or UIWebView object, iOS opens your app.
  • It’s important to understand that if your app uses openURL: to open a universal link to your website, the link does not open in your app. In this scenario, iOS recognizes that the call originates from your app and therefore should not be handled as a universal link by your app.