[ Music ]
Welcome to our session on Universal Links.
My name is Jonathan Grynspan and I work on the Core Frameworks Team at Apple.
Today, I’ll be showing you how to use universal links in your app.
We introduced the universal links in iOS 9 as a great way to provide rich content both on the web and in your app.
Today, I’ll be showing you the enhancements we’ve made to this feature.
Whether you’ve already adopted universal links in your iOS app or you’re adding them to your macOS app, you will want to pay close attention to the changes I’ll be discussing.
To begin, let’s talk about what universal links are.
Universal links are HTTP or HTTPS URLs that Apple’s operating systems recognize as pointing to resources either on the web or in your app.
This means that a single URL can represent that content, whether your users have your app installed or it just haven’t downloaded it yet.
They are great way to increase user engagement within your app.
Universal links are introduced in iOS 9 and tvOS 10.
I’m happy to announce that we are introducing them to the Mac with macOS 10.15, whether you’re using AppKit or UIKit.
I’ll tell you more about that in a moment.
Universal links are securely associated between your app and your website.
Your app adopts an entitlement in Xcode that indicates which domains it can represent and your web server adopts a single JSON file that contains more details about what parts of its domain are representable in your app.
These two ways secure handshake ensures nobody can redirect your users into their apps instead of yours.
We recommend that where you are currently using custom URL schemes, you begin migrating to universal links today.
Custom URL schemes are inherently insecure and can be abused by malicious developers.
New uses of custom URL schemes are highly discouraged.
Now that we know what universal links are, let’s talk about how to build them.
We’ll start with your web server.
Your web server must have a valid HTTPS certificate.
HTTP is not secure and cannot be used to validate an association between your app and your website.
The root certificate used to sign your HTTPS certificate must be recognized by the operating system.
Custom root certificates are not supported.
After generating your certificate and configuring your server, add your apple-app-site-association file.
This is a JSON file.
We’ll discuss this format in a moment.
When your app is installed on an Apple device, the operating system downloads this file to determine what services the server will let your app use.
The system also periodically downloads updates for this file.
Universal links are one of many services that may be included in this file.
This file should be located at HTTPS://your domain name/ .well-known/apple-app-site- association.
Other paths are deprecated.
In the past, we’ve discussed signing your apple-app-site-association file.
This has never been a necessary step to support universal links so it is now deprecated.
Support for sign JSON files and JSON files at other paths will be removed in a future release.
With that out of the way, let’s take a look at your apple-app-site-association file.
If you already have one of these files on your web server this probably looks familiar, but we have a few changes to introduce today.
At the top level is a dictionary whose keys are service types.
For universal links, the key is applinks like you see here but other services are also available.
We’ll be focusing solely on Universal Links.
Under that top level key are the apps key and the details key.
If you are targeting iOS 13, tvOS 13 and macOS 10.15, you do not need the apps key, so you can remove it.
If you are continuing to provide support for iOS 12, tvOS 12 or earlier, you’ll still need it.
For universal links, it should always be an empty array.
The details key contains an array of dictionaries, each of which represents a specific apps universal links configuration.
In the past, we supported using a dictionary structure here instead of an array but that configuration is obsolete.
Under the details key is an appID key whose value is your app identifier.
Your app identifier consists of an alphanumeric, 10 character prefix provided by Apple, a period, and your bundle identifier.
The prefix may or may not be equal to your team identifier.
Check the developer portal to confirm your app identifier.
If you have multiple apps with the same universal links configuration, you may not want to repeat the relevant JSON.
If you are targeting this year’s releases, you can reduce the size of this file by using the plural appIDs key.
The value of that key is an array of app identifiers.
If you need to support previous releases, you should keep using the singular appID key for each app.
Next is the paths key.
This key contains an array of path patterns.
Pattern matching is performed the same way it is in terminal.
The asterisk is used to indicate multiple wildcard characters while the question mark matches just one character.
Beginning this year, we are replacing the paths key with the components key.
This key’s value is an array of dictionaries, each of which contains zero or more URL components to pattern match against.
As before, you can match against the URL’s path component whose key is the forward slash.
If you need to support previous releases, you can keep the paths key.
iOS 13, tvOS 13 and macOS 10.15 will ignore it if the components key is present.
And now you can match against the URL’s fragment component whose key is the hash mark and you can match the query component whose key is the question mark.
Now many, if not most URLs out there divide their query component up into key value pairs called query items.
For the query component, you can specify a dictionary instead of a string as its value and pattern match individual query items.
URLs may repeat query item names and the operating system will require that all instances of a given query item name match your pattern.
Query items with no value and absent query items are treated by the operating system as if they have a value equal to the empty string.
For a components dictionary to match a candidate URL, all the specified components must match.
If you don’t specify a component, the operating system’s default behavior is to simply ignore that component.
So if, for example, your app doesn’t care about the fragment component of a URL, you don’t need to specify it in this file.
You may have sections of your website that are not able to be represented in your app yet.
You can exclude such subsections by specifying the exclude key with a Boolean value of true.
This key has the same behavior as a not keyword that you used in the old paths key.
That key word is not supported when using the component’s dictionary.
Here we have a few examples of URLs that we need to pattern match.
I’m working on a meal ordering app and I’m using universal links to bring users into my app from Safari.
On the left, you can see some JSON from my server and on the right, you can see some URLs.
First, I want to match all the order forms on my website which are all located on a path where the first component can be anything.
The second component is order and there are no further path components afterward.
This pattern will match a URL such as these two on the right.
Time for lunch.
Next, I know a lot of my customers are going to want to put cheese on their tacos so I’m going to match any URL where the path starts with the path component taco and where a query item named cheese is specified.
You’ll notice that I specify a question mark and an asterisk as the pattern from the query items value.
A pattern consisting of a single asterisk matches any string, including the empty string.
And a missing query item has a value equivalent to the empty string.
So to match against the string that’s at least one character long, I specify a question mark and then any additional characters are matched by the asterisk.
That matches our third URL.
The fourth and fifth URLs look very similar but there’s a good reason for that.
My website also has lots of four-digit coupon codes that the app can handle.
But if they start with a 1, I want them to stay in the browser.
Because the operating system is going to look at the available patterns from top to bottom, we’ll first mark coupon code starting with the one as excluded.
This tells the system to stop looking here if it finds a match but not to open the URL as a universal link.
Then any other coupon will match the fourth and final components dictionary.
Before we move on to your app, let’s discuss how to support an international audience.
URLs are always ASCII coded so component matching is done in ASCII as well.
If you need to match Unicode characters present and code them like you would in your URL.
Because components are present and coded, a Unicode character may be represented by more than one ASCII character, so keep that in mind when using the question mark in your patterns.
When you build your JSON, you may be tempted to provide country-specific patterns for every country you support.
This increases the size of your JSON significantly.
If your pattern-matching is consistent between countries, you can reduce the traffic to and from your server by simplifying you JSON.
For instance, if you separate your content by two-letter country code, you need to only specify two question marks where you are previously using those country codes.
Other more complex patterns like you see here can also be matched easily.
If you encounter a URL with an invalid country code or locale specific identifier, just treat it like the user’s current locale.
Beginning in this release, the operating system will prioritize apple-app-site-association downloads based on where a user is most likely to browse.
We’ll still download them all when an app is installed but at different priorities.
The top-level domains .com, .net, and .org, are high priority domains because they account for so much internet traffic worldwide.
Country code TLDs also known as ccTLDs and internationalized TLDs are also prioritized if they match the user’s current locale settings.
For example, the average user in China is more likely to visit a domain under a Chinese ccTLD than under, for example, an Italian or Russian ccTLD.
So now your server is ready to support universal links.
Let’s update your app.
Open your project in Xcode and navigate to your project settings.
Add the Associated Domains capability.
This adds a new entitlement to the selected target.
You can modify this entitlement directly from this view.
The value of this entitlement is an array of strings of the form service type: domain name.
For universal links, the service type is applinks like it was in your apple-app-site-association file.
The order of values in this array is ignored by the system.
Here, we declare that your app supports universal links for www.example.com.
When your app is then installed, the operating system will visit www.example.com looking for the apple-app-site-association file we just discussed.
If it’s present and it contains information for these apps, app identifier, then the association is confirmed.
It’s also possible to indicate wildcard support for subdomains of a given domain as shown here.
In this case, the operating system will visit example.com.
No www at this time.
Exact domains have higher priority during universal links look up than wildcard domains.
In this case, that means when the system opens a URL at www.example.com, it will try to match patterns from that domain before the ones it got from the parent domain.
Patterns from the parent domain will only be matched if no match was found at the fully qualified subdomain.
Finally, here’s an example of an internationalized domain.
Since URLs are always ASCII, your internationalized domain names will need to be encoded using Punycode.
For more information on Punycode, see RFC 3492.
Now that your app declares support for certain domains, you’ll need to actually parse the URLs as they come in.
Universal links are based on foundations and as user activity class and are handled by your app delegate.
You’ll need a handler for incoming user activities.
If you already support hand-off or other similar technologies, you may already have this method in your app delegate.
This method returns a bool.
Return true if you could successfully open the user activity and false if you couldn’t.
If you’re using UI scene, then a similar delegate method is available.
If you’re using AppKit, the signature of this method is almost identical.
Just replace UI with NS like you see here.
We’ll use UI application for the remainder of this session.
Next, we’ll check that the activityType is NSUserActivityTypeBrowsingWeb.
This helps distinguish universal links from other incoming user activities that your app may support.
Even if you don’t support other activity types now, it’s a good idea to check the activity type in case you need to support other types in the future.
The activity type looks good.
Let’s grab the webpage URL from the user activity object.
This will never be nil for a universal link and let’s build a URL components draft from the URL.
You should always parse URLs using URL components.
Using regular expressions or manually parsing a URL string may leave you vulnerable to security issues.
We’re past the guard statement so let’s examine the contents of the URL.
In this case I’m interested in the query items of the URL but you can use any of the URL’s components to root activities at this point.
If you support universal links from multiple domains, don’t forget to check the host component.
Our code is complete and our server is configured but there are few differences when using universal links on macOS.
Universal links open in the browser by default on macOS.
When they do, Safari will give the user the option to open them in your app.
If the user selects this option, your links will continue to open in your app afterward.
Unlike iOS, macOS supports launching apps present on remote volumes.
Apps on remote volumes cannot use universal links.
They must be installed locally.
If the user downloads your app from the App Store, the system will begin downloading apple-app-site-association files as soon as soon as your app is installed or updated.
If your app is developer ID-signed, the system will not begin these downloads until the user has launched your app at least once.
Because a universal link is backed by a secure association with an app identifier, only one copy of a given app will be able to handle universal links on a Mac.
Typically, this will be the copy of the app present in slash applications.
Keep that in mind anytime you need to test changes to your associated domains entitlement.
If you are on the other end of an operation and want to open a universal link, UIApplication and NSWorkspace and launch services will all automatically open them when available.
If you want to require opening a universal link in an app rather than the default browser, you can use UIApplication or NSWorkspace API as appropriate.
If these open operations fail, it means that a universal link was not available for the supplied URL.
If you’re developing a web browser from macOS, additional API will be made available to help you support universal links.
To help you make the best apps and to provide the best user experience, I’ve got a few final tips to share with you.
The first is to fail gracefully.
It’s possible you’ll be provided with URLs that represent outdated, invalid, or nonexistent content.
If you determine that a universal link can’t be opened by your app, you can often open it in Safari View Controller.
This keeps the user engaged in your app.
If Safari View Controller is not an option, consider opening the URL in Safari, or at a minimum, prompting with details about the issue.
Avoid sending the user to a blank screen.
If the user is visiting your website, use the Smart App Banner to provide a link either to the App Store or to your content.
The Smart App Banner integrates seamlessly with Safari and looks great.
Finally, if you have feedback on how we can improve universal links, I would love to hear it.
Please use the feedback assistant and let us know what we can do to make universal links even better.