Universal links with SwiftUI
It is all in the documentation, but it is a bit painful to go through and figure out what to do on your own:
- https://developer.apple.com/ios/universal-links/
- https://developer.apple.com/documentation/xcode/supporting-associated-domains
- https://developer.apple.com/documentation/xcode/allowing-apps-and-websites-to-link-to-your-content
Setup
Step 1 — Enable Associated Domains
Go to: https://developer.apple.com/account/resources/identifiers/list and enable Associated Domains
for your app identifier.
Step 2 — Create association file
Capture your 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
- https://thinkdiff.net/how-to-implement-universal-link-in-swiftui-app-7ab4e9f4f903
- https://tanaschita.com/20220801-supporting-universal-links-in-a-swiftui-application/
- https://brunoscheufler.com/blog/2020-12-13-supporting-universal-links-with-swiftui
- https://tanaschita.com/20220801-supporting-universal-links-in-a-swiftui-application/
- https://developer.apple.com/forums/thread/699401