In order to support Universal Links in our app, we’ll need to set up a Web server, that will be queried, if the app is not installed. In this case, I have already done that, myself, so I’ll walk us through what I did.
Before
This is what the server looks like, before I implement Universal Links:
META: I use Forklift as my SFTP browser, but you can use whatever you want, including the file browser in the server control panel, or even shell access. You just need to be able to upload files to the .well-known
directory.
The .well-known/acme-challenge
directories are created by Auto-SSL, the service that I use to provide HTTPS for my domains (I use A2Hosting, at the time of this entry).
Each of the main directories corresponds to a subdomain of littlegreenviper.com
(my main domain). They are the root directories of each subdomain. The index.html
file is the same one that is referenced in our project (the Web directory). There is a very slightly different version of index.html
, in each domain root. The only difference, is the displayed text, indicating which app it is being used to cover.
We’ll revisit the table of links, from the last post:
- https://iul1.littlegreenviper.com?off
- https://iul1.littlegreenviper.com?stop
- https://iul1.littlegreenviper.com?caution
- https://iul1.littlegreenviper.com?go
- https://iul2.littlegreenviper.com?off
- https://iul2.littlegreenviper.com?stop
- https://iul2.littlegreenviper.com?caution
- https://iul2.littlegreenviper.com?go
- https://iul3.littlegreenviper.com?off
- https://iul3.littlegreenviper.com?stop
- https://iul3.littlegreenviper.com?caution
- https://iul3.littlegreenviper.com?go
When you follow each link, it takes you to one of the directories, listed above, and serves the index.html
file. Each index.html
file has a JavaScript routine, that executes upon page load, and sets the “traffic lights” to the correct one for the query:
function loadHandler() {
// Fetch the command (after the question mark). The command is case-sensitive.
var command = window.location.search.split('?')[1];
// Quick accessors for the three "traffic light" divs.
var stopLight = document.getElementById('stopLight');
var cautionLight = document.getElementById('cautionLight');
var goLight = document.getElementById('goLight');
// Set the background color, according to the input command.
stopLight.style.backgroundColor = (command == 'stop') ? '#C00' : '#777';
cautionLight.style.backgroundColor = (command == 'caution') ? '#EE0' : '#777';
goLight.style.backgroundColor = (command == 'go') ? '#0C0' : '#777';
}
Yeah, it’s a pathetic little Web page, but it’s all we need, for demonstration purposes, and I wanted it to be as clear and simple as possible. We won’t be looking at these files again. They are just there, and there’s nothing more to be said.
The app-site-association
File
What we will, however, be looking at, is the .well-known
invisible directory (NOTE: You may need to switch on “Show Hidden Files,” or somesuch, in your server browser, in order to see it).
It’s very likely that the directory already exists, if you have SSL set up. If it does not exist, you should create it. Remember the prefixed period (.
), that makes the directory invisible. We will be putting a file into that directory, called “app-site-association
” (exactly that name, no prefix, no suffix). This is a JSON file that will contain the information that your system needs, in order to make that Universal Links hash table.
Before We Start
Before we get started, we need to make sure that we have our app’s full bundle ID. That means the bundle ID in the project:
With your Team ID (from your Apple Developer Team), prefixed by a period (.
).
My IDs are:
P53V4JS928.org.littlegreenviper.IUL-App
P53V4JS928.org.littlegreenviper.IUL-Scene
P53V4JS928.org.littlegreenviper.Universal-SwiftUI
(Sorry. Got sloppy)
For the App Delegate, Scene Delegate, and SwiftUI demo apps, respectively.
Next, we’ll need to create three app-site-association
files, one for each domain root directory, with that app’s bundle ID.
The App Delegate App File
First up, let’s create the JSON file for the Universal-App target:
Apple specifies an exact syntax that we need to use. Universal Links are specified with Apple’s applinks entitlement.
Here’s what the file will look like:
{
"applinks": {
"apps": [],
"details": [{
"appIDs": ["P53V4JS928.org.littlegreenviper.IUL-App"],
"components": [{
"/": "*",
"comment": "Any call can go to the app."
}
]}
]
}
}
All we are doing, here, is telling Apple that any call to this exact directory (the root for the iul1.littlegreenviper.com
domain), is associated with an iOS app that has the bundle ID listed (you can have more than one, but we’re keeping it simple). That wildcard (*
), means that any call to the subdomain, will be one that can be routed to the app, instead, if it is installed. You can also exclude stuff, if you want, but that’s a story for another day.
Very basic. You can get a lot fancier, but that’s beyond this tutorial. I just want to get you up and running. Then, you’ll have leisure to play around, and find more powerful applications, targeted to your own needs.
NOTE: The empty “apps: []
” object is required. Not sure why, but it won’t work, without.
We’ll do exactly the same, for each of the other targets:
{
"applinks": {
"apps": [],
"details": [{
"appIDs": ["P53V4JS928.org.littlegreenviper.IUL-Scene"],
"components": [{
"/": "*",
"comment": "Any call can go to the app."
}
]}
]
}
}
{
"applinks": {
"apps": [],
"details": [{
"appIDs": ["P53V4JS928.org.littlegreenviper.Universal-SwiftUI"],
"components": [{
"/": "*",
"comment": "Any call can go to the app."
}
]}
]
}
}
After
Now, we just drop each of these files into their respective .well-known
directories:
Here’s the release that contains the work to this point.
And we’re done on the server. Next, let’s add the entitlements to the apps (Universal Links are still not working).