Universal links with SwiftUI

Ondrej Kvasnovsky
4 min readJun 8, 2023

It is all in the documentation, but it is a bit painful to go through and figure out what to do on your own:

Setup

Step 1 — Enable Associated Domains

Go to: https://developer.apple.com/account/resources/identifiers/list and enable Associated Domains for your app identifier.

“Associated Domains” under app identifier.

Step 2 — Create association file

Capture your App ID Prefix and Bundle ID.

“App ID Prefix” and “Bundle ID”

Now create a new JSON file called apple-app-site-association in your local folder. The pattern for appIDs is <App ID Prefix>.<Bundle ID>. Then we have specify components, that contains mapping between path and what we want to match. In my example, I am saying that the path for which I want to open my app, is anything after /workouts/share. So, only this URL pattern will make my app to get opened: https://goatworkouts.com/workouts/share/123

{
"applinks": {
"details": [
{
"appIDs": ["5xxxxxxxx2.oxx.xxxxxxs"],
"components": [
{
"/": "/workouts/share/*"
}
]
}
]
}
}

Step 3 — XCode

Open Targets → Signing & Capabilities, search for and add “Associated Domains” to your project.

Implementation

Local setup is necessary to make everything working before uploading files to hosting servers.

Doing local setup and testing very useful, because if you upload wrong association file into your domain, it will take days before it gets refreshed on Apple CDN (which makes implementation into a very difficult and annoying activity).

Step 1 — Run a local server and expose it to the public network

Download and install NodeJS, https://nodejs.org/, we will use it to run the server that will serve the apple-app-site-association file.

Sign up for ngrok and install it on your machine (its free and perfect for this use case): https://ngrok.com/. We will use ngrok to expose your local NodeJS server to the public internet.

Create a new folder for your server code and run the following commands:

mkdir well-known-local-server
cd well-known-local-server
npm init -y
touch index.js
open .

Insert the following into index.js:

var express = require('express');
var server = express();

server.get('/.well-known/apple-app-site-association', function(request, response) {
response.sendFile(__dirname + '/apple-app-site-association');
});

server.get('/workouts/share/*', function(request, response) {
response.sendFile(__dirname + '/workouts/share/index.html');
});

server.get('/', function(request, response) {
response.sendFile(__dirname + '/index.html');
});

server.listen(80);

Step 2 — Run and verify everything is up and accessible

Run your local server:

$ node index.js

Test if the file is served from your local, you can use this:

$ curl http://localhost:80/.well-known/apple-app-site-association

Now expose your local server to the public internet using ngrog:

$ ngrok http 80

Session Status online
Account Ondrej Kvasnovsky (Plan: Free)
Update update available (version 3.3.1, Ctrl-U to update)
Version 3.3.0
Region United States (us)
Latency 61ms
Web Interface http://127.0.0.1:4040
Forwarding https://a1fe-2600-1700-1151-1f0-9c7d-fd64-aa76-6aeb.ngrok-free.app -> http://localhost:80

Connections ttl opn rt1 rt5 p50 p90
1 0 0.02 0.00 5.03 5.03

HTTP Requests
-------------

GET /.well-known/apple-app-site-association 200 OK

Now you can test if you can get access the apple-app-site-association from via ngrok.

$ curl https://a1fe-2600-1700-1151-1f0-9c7d-fd64-aa76-6aeb.ngrok-free.app/.well-known/apple-app-site-association

Step 3 — Insert your domain to XCode project

Open the target section in XCode and insert another row into Domains section. The pattern is applinks:<your-domain>, for example applinks:goatworkouts.com.

Step 4 — Add code to accept the URL

Now we can add code that gets called when the app is opened using the universal link.

@main
struct PlaygroundApp: App {

var body: some Scene {
WindowGroup {
ContentView()
.onOpenURL { url in
print(url)
}
}
}
}

If you want to start parsing the URL, here is a nice deep dive into URL parsing in swift: https://www.advancedswift.com/a-guide-to-urls-in-swift

Step 5 — Finally, open the app using the universal link

Run the app on your simulator or on your phone, and while it is running from your XCode, run the following in your terminal. I should open the URL in your app.

$ xcrun simctl openurl booted https://goatworkouts.com/workouts/share/123

Alternatively, you could put the link to message or mail client, and when tap on it, it should open the app.

Debugging

What version of file can Apple’s CDN see?

If the link did not work, you can verify what Apple CDN sees.

curl https://app-site-association.cdn-apple.com/a/v1/goatworkouts.com

Doing changes to apple-app-site-association

Every change to your file will take time to get propagated, the documentation says its days, but it seems to me it is couple of hours for sure (based on what I have observed).

When doing changes and running the server locally, it is faster to restart ngrok and use different domain. Don’t forget to change it in the XCOde project settings (applinks:<new domain>).

What others are doing?

We can look at what e.g. Facebook did, which gives us plenty of examples .

curl https://app-site-association.cdn-apple.com/a/v1/facebook.com

Other materials

--

--