Universal Links help people access your content, whether or not they have your app installed. Get the details on the latest updates for the Universal Links API, including support for Apple Watch and SwiftUI. Learn how you can reduce the size and complexity of your app-site-association file with enhanced pattern matching features like wildcards, substitution variables, and Unicode support. And discover how cached associated domains data will improve the initial launch experience for people using your app.
Hello. Welcome to "What's New in Universal Links." My name is Christopher Linn.
We've talked about universal links at WWDC before, but we'd like to give you a brief refresher.
Universal links are HTTPS or HTTP URLs that represent your content both on the Web and in your app.
They allow users to open your content in your app instead of in a Web browser, allowing you to provide a richer experience. You create them by adding a special entitlement, the Associated Domains entitlement, to your app and by adding a JSON file to your Web server. The entitlement mentions your Web server's domain name, and the Web server mentions your app's application identifier. This creates a secure two-way association between your app and your website, allowing your app to perform special tasks on behalf of your website.
Where you are currently using custom URL schemes, you should migrate to universal links as soon as possible.
Custom URL schemes are not recommended. For more detail about what universal links are and how to adopt them, check out our session from WWDC19.
After that high-level review of universal links, let's get to the fun stuff: support for new platforms.
The first and smallest platform we're adding support for this year is watchOS.
Universal links work the same way on watchOS as they do on our other platforms, but there are some platform-specific differences we want to share with you. The first is that, when using WatchKit, the API is different than when using UIKit or AppKit. And it's important to remember that your entitlements, including the Associated Domains entitlement, are applied to your WatchKit extension, not the containing WatchKit app.
On iOS, or when using Mac Catalyst, you implement the UIApplicationDelegate method "application: continue userActivity: restorationHandler" to handle incoming universal links. We've shown how to use this delegate method in previous sessions. When you want to open a universal link in another application, you use UIApplication, openURL.
However, on watchOS, you use WatchKit instead of UIKit, so to adopt on watchOS, we'll need to make some changes here.
WKExtensionDelegate's handle(_ userActivity method is responsible for handling incoming universal links. The body of this method will be fundamentally the same as on iOS and macOS.
To open a URL in another application, you use WKExtension's openSystemURL method. Just like on our other platforms, if you attempt to open a universal link in an application that's not installed, that link will fail to open.
Now, Safari is not available on watchOS except in limited contexts like Mail and Messages. And this method does not return a value or take a completion handler. So, rather than leave the user with no feedback, we present UI to the user on watchOS, and that looks like this.
Here you can see an app we've been working on that shows us the menus for all our favorite restaurants.
It lets us select our favorite foods, then order them in each restaurant's app by invoking a universal link. However, this Apple Watch doesn't have this restaurant's app installed. So when we tap the button to order, we'll see this alert, telling us to continue on the paired iPhone. So, to quickly summarize, these are the methods you use to handle and open universal links when using WatchKit.
These are the equivalent methods when using UIKit. Note that when your app is configured to use UIScene, you need to implement a different delegate method than when using only UIApplication.
And AppKit is similar to, but distinct from, UIKit. In the end, the exact API you use will depend on the platform and SDK you're using.
But-- and we're very pleased to tell you about this-- SwiftUI is adding support for universal links this year too. And that looks a little like this, no matter which platform or SDK you use. For more information about the enhancements to SwiftUI this year, please check out "What's New in SwiftUI." It's really exciting that we now have universal links on watchOS and when using SwiftUI. We also have new features available across all our platforms. We'd like to tell you about them now.
First, a quick refresher about how pattern matching works with universal links.
You can use the asterisk and question mark characters in your pattern strings to specify wildcards.
Asterisk matches zero or more characters and does so greedily. It will match as many characters as possible.
Question mark matches exactly one character. To match at least one character, use question mark followed by asterisk.
For more information about pattern matching using wildcard characters, please take a look at last year's video.
And now, on to the cool new stuff.
The first new feature we'd like to announce today is support for case-insensitive pattern matching. We've had a lot of requests for this feature. Let's say you have a pattern like this one. It will match any path that starts with the path component "sourdough" and at least one character afterward.
However, it can only match the lowercase word "sourdough." So, to support all possible mixes of uppercase and lowercase letters, you might want to add patterns for each combination of characters.
But actually, you might not. There's such a thing as too much sourdough. But you don't have to worry about carb comas anymore, because we are introducing a new key in the components dictionary: caseSensitive. Set its value to "false" to disable case-sensitive pattern matching and to enable case-insensitive pattern matching.
Case-insensitive pattern matching is available today with macOS Catalina 10.15.5 and iOS 13.5.
Next up, let's talk Unicode. URLs are always ASCII. When you encounter a URL with non-ASCII Unicode characters, what's happening behind the scenes is that you're actually encountering percent-encoded characters like you see here.
The string is converted to UTF-8, and then the hexadecimal representation of each byte in the UTF-8 sequence is emplaced in the URL.
This is the name of a tasty Szechuan dish called "ants crawling on logs," but even a fluent Chinese reader would have trouble telling that from this URL.
Wouldn't it be nice if this pattern could contain actual Chinese characters? Good news: Now it can. Add the key percentEncoded to your components dictionary and set its value to "false." This disables percent-encoding, meaning that your pattern is matched as a sequence of 32-bit Unicode code points instead of 7-bit ASCII characters. Support for Unicode is available in macOS Big Sur and iOS 14. And, of course, you can use both percentEncoded and caseInsensitive in the same pattern. We support case-insensitive Unicode pattern matching.
But this can get a little repetitive, as we have percentEncoded specified twice here and caseSensitive specified twice too.
You can now add "defaults," a dictionary containing values to apply by default to all patterns unless a pattern explicitly overrides it. Once we add this key, we can remove the percentEncoded and caseSensitive keys from each individual component. That's a lot cleaner. If it's a sibling of components, it will apply to all patterns in that components array. If it's a sibling of "details," it will apply to all universal links for this domain. And the defaults key is available for macOS Big Sur and iOS 14, along with Unicode support. Now let's look at a pattern-matching story from the real world.
This is based on a true story. My colleague Jonathan and I are building a food-ordering app that you briefly saw back when we were talking about watchOS support.
Here's a URL that we would like to pattern-match against. It contains a locale code, consisting of a two-letter language code, an underscore and a two-letter country code. This is followed by a product name, one of several food items sold on our website and in our corresponding app.
A simple pattern that would reasonably match a URL like this one looks like what you see here. Remember, the question marks match single characters, and a question mark followed by an asterisk matches one or more characters. But this pattern will match more URLs than we want. What about countries where we don't operate, languages we don't support, and products we don't sell? This pattern would match all of them.
Our first attempt at pattern matching involved hard coding the list of possible values we wanted to match against, and that might have worked if we had a small number of regions and languages that we supported and just a handful of products. But thanks to Jonathan's hard work, we operate in over 100 countries around the world.
Once we added the products we sell, all heck broke loose. If we just consider every two-letter language code, every two-letter region code and four products, we're already looking at 1.8 million possible patterns to match against, and they take up over 27 megabytes. That's because pattern matching has exponential complexity.
We took a look at this apple-app-site-association file and said, "Let's do better." We're happy to introduce a new feature of universal links that we call "substitution variables." What are they? In a nutshell, they are named lists of strings that can be matched against. These variables appear in your pattern-matching strings and represent any and all of the values you specify. Their names can contain almost any character. In a moment, you'll see why dollar signs and parentheses are restricted. Variable names are always case-sensitive when encountered in a pattern. The values you specify, on the other hand, can contain question mark and asterisk for wildcard matching but can't reference substitution variables. They don't recurse.
The values are case-sensitive if pattern matching is case-sensitive, which is the default behavior. If you have enabled case-insensitive pattern matching, then values will be matched accordingly.
To get you started, we've built in some common substitution variables. The first is alpha, which is all upper and lowercase ASCII letters.
You can see here why the dollar sign and parentheses characters are restricted. They're part of a variable's name when referenced in a pattern.
"Upper" and "lower" are the uppercase and lowercase ASCII alphabets.
"Alnum" is short for "alphanumeric" and matches all ASCII letters and the digits zero through nine.
"Digit" is decimal digits, and "xdigit" is hexadecimal digits.
These variables are equivalent to the similarly-named character classes in the standard C library. Next we have "region," which is every ISO region code recognized by Foundation's Locale type. And finally, we have "lang," which is every ISO language code.
Let's take a look at the apple-app-site-association file that we were worried about a few slides back.
Here are all those patterns as they appear within the full file. Equipped with substitution variables, we're now ready to wake up from this combinatoric nightmare.
The first thing we'll do is to add a new key-value pair under "applinks" named substitutionVariables.
The value for that key is a dictionary. The keys in that dictionary are variable names, and the values are arrays containing sets of substrings to match.
The variable we've added here is named "food" and has four possible values.
And now we're going to use this variable in these patterns. Watch me make 27 megabytes disappear.
Wow. That's an obvious improvement. We accomplished this feat by using three substitution variables. "Lang" and "region" are predefined variables, so we didn't need to define them ourselves, and "food" is the variable we defined above.
You'll note that when defining the variable, its name is in quotation marks. That's because that's the syntax for JSON keys. In a pattern, it's wrapped with a dollar sign and parentheses.
So, we could ship this, but we do have one problem: We can't sell our app or our products in Canada because we've been having a lot of trouble securing a supply of maple syrup.
So we'll add another pattern before the one we have to match the ISO region code CA, which corresponds to Canada, and we'll mark this pattern as "excluded," which tells the operating system that if a URL matches this pattern, it should not be opened by this app as a universal link. This way, we can still exclude individual combinations of variable values.
However, Jonathan was born in Canada, and he really wants to do business back home, so let's say we secure that maple syrup supply. Of course, all that syrupy goodness means that Canada needs a different menu than the other countries where we operate.
How can we handle special cases in his substitution variables? It's not hard. We just add another variable, "Canadian food," specifically for Canada. You'll notice right away that there's some overlap between the menus, and that's okay. You'll also notice we kept the exclusionary pattern we had. If we were to remove it, then the last pattern would match all values from the "food" variable in Canada, which we don't want to do.
So, we're almost ready to deploy this file. But Canada is a multilingual country. Not everyone there speaks English. At a minimum, we should support English and French, so we'll add the French translations of those Canadian food names.
Oh, that looks weird. One of the French food names has an accented letter, which means we need to deal with percent encoding. Let's clean that up.
Ah, much better. We can use the percentEncoded key we are introducing this year. It allows us to use accented letters directly in our patterns and variables.
We're done. And now I'm hungry. But I won't have to wait long to order lunch with this app, because substitution variables are available today with the macOS Catalina 10.15.6 and iOS 13.5 updates.
Now that we've spent some time talking about the contents of the apple-app-site-association file, let's talk about how it gets on to your users' devices and how we can improve that flow.
Let's say we have an iPad, and we want to download an app.
We open the App Store and select the app we want to download. There it goes.
After the app is downloaded and installed, the system checks its entitlements and sees that it needs data from one or more apple-app-site-association files.
The device opens a connection to the Web server where that file is hosted in order to download it. Now, devices have limited bandwidth, so if multiple files need to be downloaded from multiple Web servers, the device needs to download them a few at a time.
The apple-app-site-association file makes its way from the Web server to the device, is parsed by the Associated Domains daemon, and the app's universal links become active. Then the device moves on to the next queued server and so forth. But what if there's a problem with the download? Let's go back a few slides here and try downloading that file again.
The device attempts to establish a connection to the server. But let's say the Wi-Fi goes down or the server crashes or the server is simply unreachable from the device.
How far the download can proceed depends on the exact nature of the failure, but the data won't make it to the device. Eventually, the device has to give up on the download and move on to the next server. This leaves the device in an inconsistent state, where the app is installed but its universal links and other Associated Domains data are not available.
This state can persist for hours or days, until the system next attempts to update the data for that app.
But I think we can do better here. Let's go back a few slides again, but this time with an ace up our sleeve.
Once again, we pick an app on the App Store, and it's downloaded onto this device. This device sees that the app has the Associated Domains entitlement, but instead of connecting to the associated Web server, it connects to a content delivery network, or CDN, that manages Associated Domains data.
A CDN is a powerful tool and can cache large amounts of data, so it might already have the data from this Web server stored. But let's say it doesn't. It can connect to the server on behalf of this device. But again, CDNs are powerful, so it can connect to all the servers for all apps on this device simultaneously.
It can download the apple-app-site-association files for all of these domains concurrently, cache them, and send the data to the device with a single network connection. This device is looking much happier than it was the last time we tried this.
There are a number of reasons we want to use a CDN here. We've built a CDN dedicated to just Associated Domains and apple-app-site-association files, so we can fine-tune it to deliver the best experience to users.
Because a CDN caches data from multiple Web servers, we can use a single HTTP/2 connection to request all the data we need instead of a separate connection for every Web server.
Caching reduces the total load on your server from potentially millions of requests per day to just a handful.
And, because the CDN has a known-good, known-fast connection, users' experiences with your apps are overall more reliable.
Beginning with macOS Big Sur and iOS 14, your Web server will only receive requests for your apple-app-site-association files from Apple's CDN, which lives on the public Internet. However, not all servers are created equal. What if your Web server cannot be reached from the public Internet? Perhaps it's a Web server you use for pre-deployment testing or one intended only for use by employees connected to your internal network.
How can you continue to support these scenarios? We've designed a few features into our CDN support that are here to help you, and we call them "alternate modes." Alternate modes allow you to bypass the CDN and directly connect to a domain you control.
There are two alternate modes in macOS Big Sur and iOS 14, and they're distinguished by when you would use them.
The first alternate mode is called "developer mode," and it's designed for use when you're building and testing your app before you deploy it to TestFlight or end users.
The second alternate mode is called "managed mode" and is for use when your app is installed using an MDM profile. We'll be focusing today on developer mode.
For more information about managing your devices and configuring your MDM profile, watch "Managing Apple Devices." The first big difference with developer mode is that, when enabled, you can use any valid SSL certificate on your Web server, even if it is not trusted by the operating system's built-in certificate store.
This is more powerful than it sounds, because without precautions, it could allow for a man-in-the-middle attack that could leave your users vulnerable to a number of security issues.
We require that a user opt in to developer mode on any device that uses it. On iOS, watchOS and tvOS, opting in looks similar to this.
Open the Settings app and select Developer Settings. They'll appear after your device is connected for the first time to a Mac running Xcode.
Under Developer Settings, enable "Associated Domains Development." This device is now configured for developer mode.
On macOS, the process is a little different. Open Terminal and enter the command you see here: swcutil developer-mode -e true.
You'll be prompted for an administrator password or Touch ID. After granting permission, developer mode is enabled. This is a per-user operation.
Because developer mode is a global switch, we don't want to enable it for all apps. Only the app you're developing.
So it only takes effect for apps that are signed with a development profile.
Apps signed for distribution on the App Store or TestFlight or Mac apps that have been signed and notarized cannot be used with this alternate mode.
Finally, developer mode and managed mode require that you host your apple-app-site-association file in the .well-known directory, not at the root of your domain.
That's a great summary of alternate modes and how you enable one on a device. But how do you tell the system that your app should use that alternate mode for a given domain? Let's take a look at our Associated Domains entitlement. This entitlement currently has support for www.example.com.
Pretty typical. Now let's enable developer mode and managed mode for this app.
We've added separate entitlement entries for these alternate modes. The domain names we've selected are examples only. They don't need to be distinct from your public-facing domain or from each other. The domain names you use will be specific to your organization.
These new entitlement entries include a query item with the name "mode" and a value specifying the alternate mode to use. The first entitlement value we added specifies that this domain can be accessed when in developer mode. The second says that it can be accessed in managed mode.
And the third? This domain can be accessed when this device is in both developer and managed mode at the same time. You might want to use this configuration when building your internal applications.
We've gone through a lot of new stuff today. We have support for watchOS with WatchKit and all platforms with SwiftUI.
We've added several new pattern-matching features, including substitution variables, to help you build efficient, effective universal links.
And we've introduced a new CDN that speeds up and streamlines the process for downloading your apple-app-site-association files.
We're looking forward to seeing what you're able to build with these new tools. Thank you. [chimes]
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.