---
title: Strategies for opening phone URLs in iOS apps
teaser: 'Something as simple as making a phone call or opening a URL on iOS is more
  complicated than you may realise. How can we gracefully handle when things go wrong?

  '
tags: uikit,ios,mobile,user experience,design
author: Adam Sharp
published_on: 2019-05-31
---

In the beginning, there was [`-[UIApplication openURL:]`][openurl]. Whether
tapping on a link in your [Twitter feed][tweetie] or [dialling a phone
number][iphone] 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:]`][url-scheming] to build and
distribute lists of the apps installed on users' devices, and the possibility
for [regular `http:` and `https:` URLs to launch apps][universal-links], a
newer, more flexible API was needed.

Enter [`UIApplication.open(_:options:completionHandler:)`][openurl-maybe], a
method with a surprisingly complex signature for a function that just a moment
ago may have seemed simple.

[openurl]: https://developer.apple.com/documentation/uikit/uiapplication/1622961-openurl?language=objc
  "UIApplication instance method openURL - Apple Developer Documentation"
[tweetie]: https://daringfireball.net/linked/2008/11/20/tweetie-10
  "Tweetie 1.0 Review - Daring Fireball"
[iphone]: https://youtu.be/vN4U5FqrOdQ?t=1530
  "Steve Jobs demonstrating the iPhone in 2007 - YouTube"
[url-scheming]: https://useyourloaf.com/blog/querying-url-schemes-with-canopenurl/
  "Querying URL Schemes with canOpenURL - Use Your Loaf"
[universal-links]: https://developer.apple.com/documentation/uikit/core_app/allowing_apps_and_websites_to_link_to_your_content
  "Universal Links - Apple Developer Documentation"
[openurl-maybe]: https://developer.apple.com/documentation/uikit/uiapplication/1648685-open?language=swift
  "UIApplication instance method openURL with options and completion handler"

## 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:

<img
  src="https://images.thoughtbot.com/blog-vellum-image-uploads/PNhV8vkFThakOcW7UYgQ_call-support.png"
  width="414"
  alt="'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][tel],
and opens it:

<img
  src="https://images.thoughtbot.com/blog-vellum-image-uploads/yfMU0pKaQ3eCiTG326pc_phone-number-tapped.png"
  width="414"
  alt="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:

```swift
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?

[tel]: https://tools.ietf.org/html/rfc3966
  "RFC 3966: The tel URI for Telephone Numbers - IETF"

## 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:

<img
  src="https://images.thoughtbot.com/blog-vellum-image-uploads/k1DEwanRgCwLdJjqukIX_simulator-device-list.png"
  alt="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:

```swift
#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.

<aside>

What about the iPod touch? The iPhone-without-the-phone likely offers the same
experience as the iPad when it comes to making phone calls (only smaller), but
unfortunately I didn't have one available for testing.

</aside>

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][marzipan] — so it's never too late to
start designing good error handling.

[ipod-touch]: https://www.apple.com/shop/buy-ipod/ipod-touch
  "Buy iPod touch - apple.com"
[marzipan]: https://www.macrumors.com/roundup/macos-10-15/
  "macOS 10.15 - MacRumors"

## 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:

<img
  src="https://images.thoughtbot.com/blog-vellum-image-uploads/G6hBeAQSpiGitRlaxNiQ_safari-url-invalid.png"
  alt="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:

<img
  src="https://images.thoughtbot.com/blog-vellum-image-uploads/wX4HABnzSUaiQSzgSq9K_safari-ipad-popover.png"
  width="300"
  alt="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][nshipster] the
phone number. Here's what it looks like:

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

<img
  src="https://images.thoughtbot.com/blog-vellum-image-uploads/S6VMJxriRbmglBK30Gdm_copy-menu.png"
  width="414"
  alt="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][sample-code].

[nshipster]: https://nshipster.com/uimenucontroller/
  "UIMenuController - NSHipster"
[sample-code]: https://github.com/thoughtbot/phone-number-copy-example/pull/1
  "Sample application code demonstrating a URL fallback menu - thoughtbot on GitHub"
