How To Create Your Own URL Scheme

A URL Scheme is like "http://..." or "ftp://...". Those seem like a very low-level concept that you don't have much control over, but actually, you do! What sounds like an exotic topic is not so exotic at all: we are constantly using different URL Schemes, on any given day. For example when we're clicking a link to an iPhone app that launches the AppStore. Or when a friend sends us a link to a playlist and it opens in the Spotify desktop app.

In the following short tutorial, we'll look at how custom URL schemes work on macOS and iOS.

URL Schemes and Document Types

Any macOS or iOS application can register itself as a handler for any URL scheme (like "http" or "https") or document type (like "txt" files). Apart from those classics, however, an app can also register its own, custom URL scheme or document format.

If an app wants to indicate that it supports a certain document type or URL scheme, its "Info.plist" file has to be configured appropriately: the CFBundleDocumentTypes key lists the document types that the app supports, while CFBundleURLTypes is used for supported URL schemes.

In your own app, you can configure this easily via Xcode's project settings: the "Info" tab offers sections for both "Document Types" and "URL Types". The URL scheme can be any string we like (as long as it remains a valid URL format).

Xcode Project Settings

This enables the application to work with the configured types, for example, when opening files from Finder with "Open With" or handing off documents from one application to another on iOS.

Use Cases for Custom URL Schemes

In general, registering your own custom scheme allows you to route events directly to your application. When the user opens a URL with this scheme. As an example, let's look at Tower, the Git desktop client that my team makes: opening the link "gittower://openRepo/http://github.com/jquery/jquery.git" on your machine will launch Tower and open the "Clone" dialog, with the appropriate clone URL pre-filled:

A custom URL scheme in action in the Tower Git client

Another use case for us is to make registering Tower easier for our users. After purchasing a license, our customers receive an email that contains a link like this one: "gittower://activateLicense/CODE/NAME"

This will launch Tower (or bring it to front) and open the registration dialog with the license information pre-filled. This is much for comfortable than fumbling with copy and paste (only to notice that you missed a character or included unwanted ones...).

On iOS, the use cases are very similar: applications also make use of custom URL schemes to launch an application and then display a certain screen inside the app.

To make a long story short: custom URLs are a great way to deep-link into your application!

An Example App

Let's get our hands dirty and create our own application that handles a custom URL scheme. Let's call the app CustomURLScheme and have it handle a scheme called (of course!) foo.

The sample code for this little tutorial can be found here.

Registering Our Custom URL Scheme

The first step is to register the application as a handler for our custom URL scheme, in our project's Info.plist file:

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Viewer</string>
        <key>CFBundleURLName</key>
        <string>com.example.CustomURLScheme</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>foo</string>
        </array>
    </dict>
</array>

Thereby, we ceremoniously offer to take the role of "Viewer" for the URL scheme foo.

For more information and a detailed explanation of all the possible configuration keys, you can have a look at Apple's Property List Key Reference.

Handling Events from Your URL Scheme

The next step is to tell our application how to handle events that come in via our URL scheme.
For iOS applications, this is as simple as implementing the following delegate:

func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool

For macOS applications, we need to tell NSAppleEventManager that our application wants to receive events for opening URLs and provide a callback method to handle the event.

We first create an empty method with the expected signature in our AppDelegate class:

class AppDelegate: NSObject, NSApplicationDelegate {

  func applicationDidFinishLaunching(_ aNotification: Notification) {
  }

  func applicationWillTerminate(_ aNotification: Notification) {
  }

  func handleAppleEvent(event: NSAppleEventDescriptor, replyEvent: NSAppleEventDescriptor) {
  }
}

Then we call NSAppleEventManager's setEventHandler method from applicationDidFinishLaunching as follows:

func applicationDidFinishLaunching(_ aNotification: Notification) {
  NSAppleEventManager.shared().setEventHandler(self, andSelector: #selector(self.handleAppleEvent(event:replyEvent:)), forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL))
}

Now, if you'd build and run the application, the event would be correctly passed to our callback method - but it's still empty and won't do anything.

The callback methods receive the incoming event as event: NSAppleEventDescriptor. NSAppleEventDescriptor has lots of properties and methods. If you only care for the URL, the following implementation will do the trick:

func handleAppleEvent(event: NSAppleEventDescriptor, replyEvent: NSAppleEventDescriptor) {
  guard let appleEventDescription = event.paramDescriptor(forKeyword: AEKeyword(keyDirectObject)) else {
      return
  }

  guard let appleEventURLString = appleEventDescription.stringValue else {
      return
  }

  let appleEventURL = URL(string: appleEventURLString)

  print("Received Apple Event URL: \(appleEventURL)")
}

So the final implementation for macOS looks like this:

class AppDelegate: NSObject, NSApplicationDelegate {

  func applicationDidFinishLaunching(_ aNotification: Notification) {
    NSAppleEventManager.shared().setEventHandler(self, andSelector: #selector(self.handleAppleEvent(event:replyEvent:)), forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL))
  }

  func applicationWillTerminate(_ aNotification: Notification) {
  }

  func handleAppleEvent(event: NSAppleEventDescriptor, replyEvent: NSAppleEventDescriptor) {
    guard let appleEventDescription = event.paramDescriptor(forKeyword: AEKeyword(keyDirectObject)) else {
        return
    }

    guard let appleEventURLString = appleEventDescription.stringValue else {
        return
    }

    let appleEventURL = URL(string: appleEventURLString)

    print("Received Apple Event URL: \(appleEventURL)")
  }
}

Build and run the application and it should print the received URL to the debug console.

Once you have the URL, it's up to you to translate it into an action you want your application to perform.

Registering an App as the Default Handler

Apart from our own gittower scheme, Tower supports two additional ones: github-mac and sourcetree, because these schemes are used on github.com and bitbucket.com to open clone URLs in a desktop application. Of course we don't "blindly" overwrite other handlers! Users can explicitly choose to let Tower handle these URLs from GitHub and Bitbucket.

This is done with an interesting part of the CoreServices framework, the Launch Services API. Although the API is in C, it's quite easy to write a Swift wrapper for the required methods:

import Foundation
import CoreServices

class LaunchServices {

  class func applicationsForURLScheme(scheme: String) -> Array<String> {
    if let applications = LSCopyAllHandlersForURLScheme(scheme as CFString) {
      return applications.takeUnretainedValue() as Array<AnyObject> as! Array<String>
    }

    return []
  }

  class func defaultApplicationForURLScheme(scheme: String) -> String? {
    if let defaultApplication = LSCopyDefaultHandlerForURLScheme(scheme as CFString) {
      return defaultApplication.takeUnretainedValue() as String
    }

    return nil
  }

  class func setDefaultApplicationForURLScheme(bundleIdentifier: String, scheme: String) -> Bool {
    let status = LSSetDefaultHandlerForURLScheme(scheme as CFString, bundleIdentifier as CFString)
    return (status == 0)
  }
}

This helper class provides the following core functionalities:

  • applicationsForURLScheme – Retrieve a list of application bundle identifiers that have declared support for a particular URL scheme
  • defaultApplicationForURLScheme – Return the application bundle identifier of the current default handler of a particular URL scheme
  • setDefaultApplicationForURLScheme – Set the default handler of a particular URL scheme to a new application bundle identifier

The macOS example project demonstrates how to use this class: it displays a list of all applications for a particular URL scheme, with the default application preselected (don't worry: playing with the selection input does not change the default; it is read-only in this example).

Go Ahead, Create Your Own Scheme

Custom URL schemes are a great way to deep-link into your application and trigger actions. They are easy to set up (especially on iOS) and provide your users with convenient shortcuts when coming from other applications.

Have fun creating your own URL schemes!