WWDC2019 Session 717

Transcript

[ 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.
And there's no JavaScript or
custom URL schemes required to
support it.
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.
Thank you.