Links and hyperlinks are common in many apps, especially those that are text-heavy. While many UX experts recommend minimizing navigation away from your app, there are times when it is necessary to allow users to visit a web page or another installed app.
On the surface, displaying links in SwiftUI feels restricted to just Link
. And it could feel tricky to control or override a link should we want to reroute for some reason, especially if that link is coming from dynamic content.
Enter openURL
, an Environment Key Value that, when summoned, allows you to do as it is named: open a URL. Particularly allowing you to do so somewhat programmatically within a View
.
But openURL
is of type OpenURLAction
, which is exposed to us and gives us the ability to override openURL
in the Environment.
Let’s dig in, together, to understand how these work and for what purposes would we want to use them. We’ll even learn a few creative tricks that can bust open how we can incorporate links better in our app experiences.
openURL at a Glance
As mentioned, we do have the Link
component at our disposal. But let’s say we want to programmatically trigger a URL, like from a Button
. We can pull openURL
and then call it with a URL passed in:
struct DemoView {
@Environment(\.openURL) private var openURL
var body: some View {
Button("Go to site") {
let urlString = "https://captainswiftui.substack.com"
if let url = URL(string: urlString) {
openURL(url)
}
}
}
}
At a baseline, that’s all it takes. One other addition is a completion handler that returns a success boolean:
openURL(url) { success in
print("Success: \(success)")
}
This seems simple, but openURL
raises a few questions:
Is there anything else
openURL
can do?Why is
openURL
in the Environment?
OpenURLAction at a Glance
Let’s start with investigating what openURL
can really do. To do that, we look to openURL
’s actual type: OpenURLAction
. The OpenURLAction
type initializes with a handler that takes a URL as a parameter and returns a value from the enum OpenURLAction.Result.
This is where things begin to get interesting. The Result
cases are as follows (and described in the vernacular of a Sopranos character):
handled
: in other words “do some of this, some of that and bada bing, bad boom, all done, it’s taken care of” and we move ondiscarded
: “we don’t want it, we don’t like it, we’re outta here” with a failuresystemAction
: “looks good, do the usual with this one” and does what openURL would normally do with the URLsystemAction(URL)
: “looks good, although we may have modified it. Would appreciate it if you’d look the other way and do the normal thing, ok?”
// The following updates openURL with an action that
// prints the URL's absoluteString before moving on
// to the systemAction (e.g. open in browser)
.environment(\.openURL, OpenURLAction { someURL in
print(someURL.absoluteString)
return .systemAction
})
With these four Result
cases, we can control the destiny of any URL. Whether cancelling it to altering its identity and function altogether.
Imagine…
Imagine we are reading Markdown files containing hyperlinks and displaying the document in our UI. Normally, tapping a link would open the corresponding URL (calling systemAction
by default).
But let’s say we wanted to direct the user to a sub-feature within our app instead of to the web version in some browser? We could intercept that URL, recognize it, and decide to take another action instead (e.g. pass a value to a NavigationPath
or trigger an alert
). And once we’re done, let the system know it’s been handled.
We could, if we wanted to, make interception even easier by having the Markdown author pass in custom URL text. Suppose we have a music player feature in our app. We can inform the content author to set the URL to something like “play://beethoven”. Our OpenURLAction
can look for and recognize that URL, and then trigger playing the music.
.environment(\.openURL, OpenURLAction { url in
if let scheme = url.scheme {
if scheme == "play" {
MusicPlayer.play(url)
return .handled
}
}
return .systemAction
})
Setting the Standard Environmentally
Now that we understand just the level of URL handling and manipulation that is available to us, we can better understand why we find openURL
in Environment. By resetting what it should do in a given Environment, we are re-standardizing the new URL expectations.
And since things like Link
and AttributedStrings
in Text
components use openURL
under the hood, it take does set the expectations throughout the UX.
It comes down to this: openURL
offers a central point of URL strategizing and routing to be run through.
Injecting and/or Modifying URL Components
Another neat trick could be editing a URL before passing it forward. For instance, adding a user parameter or analytics tag.
A great example is this: let’s say you have various server environments setup (prod, beta, qa, etc) but the source from where the URL is being used is static (e.g. you’re reading from a Markdown file). We can have a custom OpenURLAction
capture that incoming URL, check some app property/manager/whatever to see what environment we’re running against, and then edit the URL accordingly.
So if you have a URL of http://example.com, but need to hit http://beta.example.com, you can do that here!
// Quick demonstration of modifying the URL
.environment(\.openURL, OpenURLAction { url in
var newURL = url
newURL.append(queryItems: [
.init(name: "env",
value: currentEnvironment)
])
return .systemAction(newURL)
})
App Routing and Navigation Strategies
Developing a URL strategy could include not navigating to the web, but rather somewhere within our app. We could start creatively thinking about how we can take advantage of such manipulation at the app architecture level. If we can benefit from customizing how we react to URLs, we may want to allow that to influence how we setup our navigation.
For example, we may want to build routing capabilities and/or an organized and accessible NavigationPath
.
Continuing with our Markdown example, we could then strategically place and name links that can dynamically open different areas of our app from one document to another.
Non-Nav Uses
Because we can capture a URL and then tell the system whether it’s handled or not, this gives us quite a bit of freedom to define what “handled” means to us and users. Maybe it doesn’t mean navigating at all.
For instance, maybe tapping a link could just add to a tally. Or maybe it can trigger sending a message, but in the background. We’d probably want some form of feedback, but doesn’t necessarily require navigation.
Conclusion: .handled
I bet you didn’t think opening a URL could be so customizable nor complex. And yet, it’s one of those features that really opens up some interesting possibilities for a developer.
Understanding the capabilities of openURL
allows you to harness its potential creatively. Even if the functionality does not seem immediately relevant to your current requirements, it can inspire innovative solutions and enrich your app’s user experience.