Home What's New with Universal Links
Post
Cancel

What's New with Universal Links

iOS 13, iOS 14 What’s New with Universal Links & Setting Up a Local Testing Environment

Photo by [NASA](https://unsplash.com/@nasa?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText){:target="_blank"}

Photo by NASA

Preface

For a service that has both a website and an app, the functionality of Universal Links is crucial for user experience, achieving seamless integration between the web and the app. However, it has always been set up simply without much emphasis. Recently, I spent some time researching and documenting some interesting things.

Common Considerations

In services I have worked on, the consideration for implementing Universal Links is that the app does not have complete website functionality. Universal Links recognize the domain name, and as long as the domain name matches, the app will open. To address this issue, you can exclude URLs on the app that do not have corresponding functionality on the website. If the website service URLs are very specific, it may be better to create a new subdomain for Universal Links.

When does apple-app-site-association update?

  • For iOS < 14, the app will query the apple-app-site-association of the Universal Links website during the first installation or update.
  • For iOS ≥ 14, Apple CDN caches and periodically updates the apple-app-site-association of the Universal Links website. The app will fetch it from Apple CDN during the first installation or update. However, there may be a problem here as the apple-app-site-association on Apple CDN may still be outdated.

Regarding the update mechanism of Apple CDN, after checking the documentation, there is no mention of it. In a discussion, the official response was only “regular updates” with details to be released in the documentation… but I have not seen it yet.

I personally think it should be updated at least every 48 hours… so if you make changes to apple-app-site-association, it is recommended to update it online a few days before the app update is released.

apple-app-site-association Apple CDN Confirmation:

1
2
Headers: HOST=app-site-association.cdn-apple.com
GET https://app-site-association.cdn-apple.com/a/v1/your-domain

You can see the current version on Apple CDN. (Remember to add Request Header Host=https://app-site-association.cdn-apple.com/)

iOS ≥ 14 Debug

Due to the aforementioned CDN issue, how can we debug during the development phase?

Fortunately, Apple provides a solution for this part, otherwise it would be really frustrating not being able to update in real-time; we just need to add ?mode=developer after applinks:domain.com, and there are also managed (for enterprise internal APP) or developer+managed modes that can be set.

After adding mode=developer, the app will fetch the latest app-site-association directly from the website every time you Build & Run on the simulator.

If you want to Build & Run on a real device, you need to go to “Settings” -> “Developer” -> enable the “Associated Domains Development” option.

⚠️ There is a pitfall here, app-site-association can be placed in the root directory of the website or in the ./.well-known directory; but in mode=developer, it will only look for ./.well-known/app-site-association, which made me think it wasn’t working.

Development Testing

If you are using iOS <14, remember that if you have made changes to app-site-association, you need to delete it and then Build & Run the app again to fetch the latest one. For iOS ≥ 14, please refer to the aforementioned method and add mode=developer.

For better modification of the app-site-association content, you can modify the file on the server yourself. However, for those of us who sometimes cannot access the server side, testing universal links can be very troublesome. You have to constantly bother backend colleagues for help, and it becomes necessary to be very certain about the app-site-association content before going live, as constantly changing it can drive your colleagues crazy.

Setting up a Local Simulation Environment

To solve the above problem, we can set up a small service locally.

First, install nginx on your Mac:

1
brew install nginx

If you haven’t installed brew yet, you can do so by running:

1
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

After installing nginx, go to /usr/local/etc/nginx/ and open the nginx.conf file for editing:

1
2
3
4
5
6
7
8
9
10
11
...omitted
server {
        listen       8080;
        server_name  localhost;
#charset koi8-r;
#access_log  logs/host.access.log  main;
location / {
            root   /Users/yourusername/Documents;
            index  index.html index.htm;
        }
...omitted

Around line 44, change the root in the location / to the directory you want (using Documents as an example here).

Listening on port 8080, no need to change if there are no conflicts.

Save the changes, then start nginx by running:

1
nginx

To stop it, run:

1
nginx -s stop

If you make changes to nginx.conf, remember to run:

1
nginx -s reload

to restart the service.

Create a ./.well-known directory inside the root directory you just configured, and place the apple-app-site-association file inside ./.well-known.

⚠️ If .well-known disappears after creation, please note that on Mac, you need to enable “Show hidden files” feature:

In the terminal, run:

1
defaults write com.apple.finder AppleShowAllFiles TRUE

Then run killall finder to restart all finders.

⚠️ The apple-app-site-association file may not have an extension, but it actually has the .json extension:

Right-click on the file -> “Get Info” -> “Name & Extension” -> Check for the extension and uncheck “Hide extension” if necessary.

Once confirmed, open the browser to test if the following link can be downloaded successfully: apple-app-site-association at:

1
http://localhost:8080/.well-known/apple-app-site-association

If the download is successful, it means the local environment simulation is successful!

If you encounter a 404/403 error, please check if the root directory is correct, if the directory/file is placed correctly, and if the apple-app-site-association file accidentally includes the extension (.json).

Register & Download Ngrok

[ngrok.com](https://dashboard.ngrok.com/get-started/setup){:target="_blank"}

ngrok.com

Extract the ngrok executable

Extract the ngrok executable

Access the [Dashboard page](https://dashboard.ngrok.com/get-started/setup){:target="_blank"} to execute Config settings

Access the Dashboard page to execute Config settings

1
./ngrok authtoken YOUR_TOKEN

After setting up, run:

1
./ngrok http 8080

Because our nginx is on port 8080.

Start the service.

At this point, you will see a window showing the status of the service startup, and you can obtain the public URL assigned for this session from the Forwarding section.

⚠️ The assigned URL changes every time you start, so it can only be used for development testing purposes.

Here, we will use the assigned URL for this session https://ec87f78bec0f.ngrok.io/ _as an example.

Return to the browser and enter https://ec87f78bec0f.ngrok.io/.well-known/apple-app-site-association to see if you can successfully download and view the apple-app-site-association file. If everything is fine, you can proceed to the next step.

Enter the ngrok-assigned URL into the Associated Domains applinks: settings.

Remember to add ?mode=developer for testing purposes.

Rebuild & Run the APP:

Open the browser and enter the corresponding Universal Links test URL (e.g., https://ec87f78bec0f.ngrok.io/buy/123) to see the results.

If a 404 page appears, ignore it as we don’t actually have that page. We are testing if iOS matches the URL functionality as expected. If you see “Open” above, it means the match is successful. You can also test the reverse scenario.

Click “Open” to open the APP -> Test successful!

After testing OK in the development phase, confirming the modified apple-app-site-association file and handing it over to the backend for uploading to the server can ensure everything goes smoothly~

Finally, remember to change the Associated Domains applinks to the correct trial site URL.

Additionally, we can also check whether the apple-app-site-association file is requested each time the APP Build & Run is executed from the ngrok status window:

Before iOS < 13:

The configuration file is relatively simple, and only the following content can be set:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
  "applinks": {
      "apps": [],
      "details": [
           {
             "appID" : "TeamID.BundleID",
             "paths": [
               "NOT /help/",
               "*"
             ]
           }
       ]
   }
}

Replace TeamID.BundleId with your project settings (ex: TeamID = ABCD, BundleID = li.zhgchg.demoapp => ABCD.li.zhgchg.demoapp).

If there are multiple appIDs, you need to add multiple sets.

The paths section represents the matching rules, supporting the following syntax:

  • *: Matches 0 to multiple characters, ex: /home/* (home/alan…)
  • ?: Matches 1 character, ex: 201? (2010~2019)
  • ?*: Matches 1 to multiple characters, ex: /?* (/test, /home…)
  • NOT: Excludes in reverse, ex: NOT /help (any URL but /help)

You can decide on more combinations based on the actual situation, for more information, refer to the official documentation.

- Please note, it is not Regex and does not support any Regex syntax. - The old version does not support Query (?name=123) and Anchor (#title). - Chinese URLs must be converted to ASCII before being placed in paths (all URL characters must be ASCII).

After iOS ≥ 13:

The functionality of the configuration file has been enhanced, with added support for Query/Anchor, character sets, and encoding handling.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
"applinks": {
  "details": [
    {
      "appIDs": [ "TeamID.BundleID" ],
      "components": [
        {
          "#": "no_universal_links",
          "exclude": true,
          "comment": "Matches any URL whose fragment equals no_universal_links and instructs the system not to open it as a universal link"
        },
        {
          "/": "/buy/*",
          "comment": "Matches any URL whose path starts with /buy/"
        },
        {
          "/": "/help/website/*",
          "exclude": true,
          "comment": "Matches any URL whose path starts with /help/website/ and instructs the system not to open it as a universal link"
        },
        {
          "/": "/help/*",
          "?": { "articleNumber": "????" },
          "comment": "Matches any URL whose path starts with /help/ and that has a query item with name 'articleNumber' and a value of exactly 4 characters"
        }
      ]
    }
  ]
}

Copied content:

appIDs is an array that can contain multiple appIDs, so you don’t have to repeat the entire block as before.

WWDC mentioned compatibility with the old version, when iOS ≥ 13 reads the new format, it will ignore the old paths.

The matching rules are now placed in components; supporting 3 types:

  • /: URL
  • ?: Query, ex: ?name=123&place=tw
  • #: Anchor, ex: #title

They can be used together. For example, if only /user/?id=100#detail needs to jump to the app, it can be written as:

1
2
3
4
5
{
  "/": "/user/*",
  "?": { "id": "*" },
  "#": "detail"
}

The matching syntax remains the same as the original syntax, also supporting *, ?, ?*.

Added comment field for comments to help identification. (But please note that this is public and visible to others)

Reverse exclusion is now specified with exclude: true.

Added caseSensitive feature to specify whether the matching rules are case-sensitive, default: true. This can reduce the number of rules needed if required.

Added percentEncoded as mentioned earlier, in the old version, URLs needed to be converted to ASCII and placed in paths first (if it’s Chinese characters, it will look ugly and unrecognizable); this parameter specifies whether to automatically encode for us, default is true. If it’s a Chinese URL, it can be directly included (ex: /customer service ).

For detailed official documentation, refer to this.

Default character sets:

This is one of the important features of this update, adding support for character sets.

System-defined character sets:

  • $(alpha): A-Z and a-z
  • $(upper): A-Z
  • $(lower): a-z
  • $(alnum): A-Z, a-z, and 0–9
  • $(digit): 0–9
  • $(xdigit): Hexadecimal characters, 0–9 and a,b,c,d,e,f,A,B,C,D,E,F
  • $(region): ISO region codes isoRegionCodes, Ex: TW
  • $(lang): ISO language codes isoLanguageCodes, Ex: zh

If our URL has multiple languages and we want to support Universal links, we can set it up like this:

1
2
3
"components": [        
     { "/" : "/$(lang)-$(region)/$(food)/home" }      
]

This way, both /zh-TW/home and /en-US/home will be supported, making it very convenient without having to write a long list of rules!

Custom character sets:

In addition to the default character sets, we can also define custom character sets for increased configurability and readability.

Simply add substitutionVariables in applinks:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
  "applinks": {
    "substitutionVariables": {
      "food": [ "burrito", "pizza", "sushi", "samosa" ]
    },
    "details": [{
      "appIDs": [ ... ],
      "components": [
        { "/" : "/$(food)/" }
      ]
    }]
  }
}

In this example, a custom food character set is defined and used in subsequent components.

The example can match /burrito, /pizza, /sushi, /samosa.

For more details, refer to this article in the official documentation.

No inspiration?

If you don’t have any inspiration for the content of the configuration file, you can secretly refer to the content of other websites. Just add /app-site-association or /.well-known/app-site-association to the homepage URL of the service website to read their configuration.

For example: https://www.netflix.com/apple-app-site-association

Supplement

In the case of using SceneDelegate, the entry point for opening universal links is in the SceneDelegate:

1
func scene(_ scene: UIScene, continue userActivity: NSUserActivity)

Instead of in AppDelegate:

1
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool

Further Reading

References

If you have any questions or suggestions, feel free to contact me.

===

本文中文版本

===

This article was first published in Traditional Chinese on Medium ➡️ View Here



This post is licensed under CC BY 4.0 by the author.

iOS Cross-Platform Account and Password Integration to Enhance Login Experience

Reinstallation Note 1 - Laravel Homestead + phpMyAdmin Environment Setup