Strategies for opening phone URLs in iOS apps

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:

'Call Support' screen with a list of phone numbers for different support regions

Tapping “Australia” selects the phone number, forms it into a tel: URL, and opens it:

An alert showing the tapped phone number with a prompt to either Call or Cancel

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:

Xcode's Destination menu displaying a list of physical and simulated devices

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:

An alert from Safari with the message 'Safari cannot open the page because the address is invalid'

Thankfully, on a real iPad, we see something far more helpful — Safari is even aware that Skype is installed and can make phone calls:

A popover in Safari on iPad with the options 'Send Message', 'Add to Contacts', 'Copy Phone Number' and 'Skype'

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)"
}

The 'Call Support' screen with a phone number selected and a Copy option visible

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.