Dec 13, 2020

Supporting Universal Links with SwiftUI

Imagine you're building a messaging app where you can invite people with a link. Normally, links are opened in Safari, which doesn't give your users the seamless experience you might hope for.

Luckily, you can implement a feature called universal links (often referred to as deep links), so if a user taps on a link associated with your application, the system will open your app in exactly the right context (i.e. a confirmation view to join a chat group).

If your app is not installed, the link will be opened in Safari, which you could use to let potential users know where to download it or provide a web-based experience. This is an advantage over custom URL schemes, which would offer similar functionality, but won't work if your app is not installed.

Before we handle universal links in your iOS application, we need to associate the domain you are going to use for universal links with it. This is done in the following step.

↔️ Hosting apple-app-site-association

When a user installs your application, the system will check that the associated domains your application uses serve an apple-app-site-association, which confirms that the domain permits universal links to be used with it. We need this two-way association to make sure that universal links will only be enabled if both your app and the domain support it, otherwise anyone could force potentially destructive actions by redirecting links to other applications.

{
  "applinks": {
    "details": [
      {
        "appIDs": ["<Your Team Identifier>.<App Bundle Identifier>"],
        "components": [
          {
            "/": "/link/*",
            "comment": "Universal links"
          },
          {
            "/": "/help/*",
            "exclude": true
          }
        ]
      }
    ]
  }
}

The association file is encoded as JSON, and can contain multiple sections for different services, we'll focus on applinks for this guide. In the details, you can specify all applications that are authorized to be opened instead of Safari, when the links match specific components.

Supported components include URL path, search query, and fragment, more on this here. You can also exclude specific components, which should always be served via the web.

In our example, all links to our associated domain, starting with /link, will be regarded as universal links, and preferably opened in your application.

To make this information available, you need to host the apple-app-site-association file (without extensions) as a static file available at https://<your domain>/.well-known/apple-app-site-association. If you want to support different sub-domains, you need to host an association for each of those!

The association file also has to be served with the Content-Type application/json, which you might have to set manually (as the file does not have a file ending), depending on your web server, and without any redirects.

With the release of macOS Big Sur and iOS 14, your devices no longer directly fetch the file, instead, requests are sent to Apple's CDN and proxied to your web server. This also means that you cannot use non-public domains without using alternate modes.

To conclude

  • host the apple-app-site-association on each of your (sub)-domains you want to use with universal links
  • the file has to be hosted at https://<your domain>/.well-known/apple-app-site-association, without redirects, and as application/json
  • the application identifiers in the file need to be constructed by combining your team identifier (can be checked in the Apple Developer membership page) and bundle identifier (usually reversed domain and application name, can be found in Xcode)

To complete the two-way association, we need to add your previously configured associated domains to your application. This can be done by adding the Associated Domains capability, which will create the Associated Domains Entitlement in your app's entitlements file.

Supporting universal links for example.com would require you to add applinks:example.com to the associated domains. Note the service prefix, which defines the feature to be used with the domain.

With iOS 14, Apple introduced a new view modifier to SwiftUI, onOpenURL. Specifically designed for universal links, it allows to register a handler that will be called whenever your application is opened using a universal link, regardless of its state (foreground, background, force-quit).

@main
struct ExampleApplication: App {
	@State var tappedUrl: String = ""
  var body: some Scene {
    WindowGroup {
      VStack {
				Text("Hello world")
				Text(tappedUrl)
			}
        .onOpenURL { url in
          tappedUrl = url.absoluteString
        }
    }
  }
}

This is a relatively simple example, once you tap a universal link, it will open your app and display this link. You can nest the onOpenURL anywhere you'd like, but it will only be triggered if this view is rendered, so only if that specific part of your application would display (e.g. if you show a different view tree to unauthenticated and authenticated users, you need to make sure to add your onOpenURL to the top-level view if it should be handled for both cases).

Unfortunately, it's not really straightforward to debug universal links. If you're tapping a link and it opens in Safari instead of your application, this could have multiple reasons:

  • the association file on your domain was missing or did not get served correctly
  • your application did not define the URL domain as an associated domain

Please check that you set up the association file correctly, one small mistake like an incorrect path, not using TLS, redirecting requests, sending the wrong content type, using the wrong team id or bundle identifier, etc. could be causing problems, so everything needs to be just right.

And if this still doesn't work but you're sure everything's set up correctly, there's probably some caching involved with detecting supported domains after installing your application, so it might help to remove the application, restart the device, and install it again. This should at least attempt to fetch the latest association file and reconcile it with the latest build of the app.

It took me a couple of attempts, but after installing the application and tapping on a universal link, it opens the application as expected. I personally think the system uses too much magic, as it's really challenging to comprehend what's happening in the background, and even harder to debug when it's not working, but it is quite a joy seeing the links work.

📚 Helpful References