In the beginning, there was -[UIApplication openURL:]
. Whether
tapping on a link in your Twitter feed or dialling a phone
number from your contacts list, the magic always happened in
-openURL:
. These days, things are a little more complicated. With many apps
having abused -[UIApplication canOpenURL:]
to build and
distribute lists of the apps installed on users’ devices, and the possibility
for regular http:
and https:
URLs to launch apps, a
newer, more flexible API was needed.
Enter UIApplication.open(_:options:completionHandler:)
, a
method with a surprisingly complex signature for a function that just a moment
ago may have seemed simple.
Let’s build a phone support screen
Imagine you have a successful app with a growing user base in multiple regions. You’ve received enough support requests on your App Store review page that it’s about time to start offering real technical support from within your app. Let’s make a screen that lists the available phone support numbers, which when tapped will launch users right into a phone call:
Tapping “Australia” selects the phone number, forms it into a tel:
URL,
and opens it:
Looking good. We have phone numbers, we can tap them, and they’ll launch the user’s phone app to make the call. Here’s what that might look like in code:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let region = supportRegions[indexPath.row]
UIApplication.shared.open(region.phoneNumber.url) { success in
if success {
// Nothing to do
} else {
// What now?! TODO: Handle errors
tableView.deselectRow(at: indexPath, animated: true)
}
}
}
Hold on a second: what’s the success
parameter all about? What does it mean
if it’s false
? And how should we handle it?
Not all iOS devices can make phone calls
There are many different kinds of iOS device. When running this sample app in Xcode, here’s the list of devices I can simulate:
Which, if any, of these devices might fail to open a phone number URL?
The iOS Simulator is a powerful tool for building and testing iOS apps. It’s also usually the very first encounter a developer will have with the idea of a device that can’t make phone calls. However, we don’t deploy our finished apps to simulators, which can easily result in code like this:
#if targetEnvironment(simulator)
showAlert("Not available in the simulator.")
#else
UIAplication.shared.open(/* ... */)
#endif
This is easy, but not really sufficient, and it hides the fact that there might be real devices out there that still can’t make phone calls.
Among the devices pictured above are both iPhones and iPads. “But this app is only available on the iPhone!” you might say. This is a common refrain, but even if you’d prefer not to support iPad, did you know that all iPhone apps are available in the iPad app store, where they run scaled up for the larger display? iPads aren’t phones, but they can make and receive calls from a paired iPhone that’s signed into the same iCloud account, through a broad umbrella of features collectively known as Continuity. Thankfully, even though an iPad may not be configured to make and receive calls, iOS will always successfully “open” the phone URL and display an informative message.
We haven’t yet determined conclusively what kind of device might cause the phone URL to fail to open, but this exploration into the category of non-iPhone iOS devices should make something clear: if we ignore error handling, we might be designing a subpar experience for some of our users with device configurations we didn’t anticipate. In fact, as soon as this year, we might be running iOS apps natively on the Mac — so it’s never too late to start designing good error handling.
If we can’t make a phone call, what next?
Let’s start by seeing how Safari behaves when we can’t make phone calls. When opening a phone URL on a web page in an iOS simulator, we’re greeted with this decidedly unhelpful alert:
Thankfully, on a real iPad, we see something far more helpful — Safari is even aware that Skype is installed and can make phone calls:
This is a really helpful menu! Copy the number, and we can paste it into a note or email it to ourself. Add it to our contact list and it might automatically sync to our phone. Or we can share it by sending a text. These are all great options, but unfortunately this menu is non-standard, and would take a bit of effort to reproduce and maintain. What if we start simple, and just offer the ability to copy the number?
A graceful fallback
With just a little bit of code, we can add support for copying the phone number. Here’s what it looks like:
override var canBecomeFirstResponder: Bool {
return true
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return action == #selector(copy(_:))
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let region = regions[indexPath.row]
UIApplication.shared.open(region.phoneNumber.url) { success in
if success {
// ...
} else if let cell = tableView.cellForRow(at: indexPath) {
let menu = UIMenuController.shared
menu.setTargetRect(cell.frame, in: tableView)
menu.setMenuVisible(true, animated: true)
} else {
tableView.deselectRow(at: indexPath, animated: true)
}
}
}
override func copy(_ sender: Any?) {
guard let indexPath = tableView.indexPathForSelectedRow else { return }
let region = regions[indexPath.row]
UIPasteboard.general.string = "\(region.phoneNumber)"
}
We’ve taken a tour of the history of opening URLs on iOS, seen that iOS is more varied and complex than we sometimes realise, and surveyed some patterns and anti-patterns for error handling in mobile applications. Where we ended up is a graceful fallback mechanism that gives users an affordance and an opportunity, avoiding the trap of a confusing or frustrating experience.
You can find the code for this sample application on GitHub.