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).
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:
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 schemedefaultApplicationForURLScheme
– Return the application bundle identifier of the current default handler of a particular URL schemesetDefaultApplicationForURLScheme
– 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!
Great article for macOS apps but not so much for iOS.
iOS is moving away from custom URL schemes and towards universal links: https://developer.apple.com/ios/universal-links/
The main difference is that a URL scheme only works if the app is installed. A universal link, being a regular link to a web page, works regardless of whether or not the app is installed. This leads to a much better user experience.
For those wondering about other platforms, this is also supported on Windows and Android. Android also supports linking to applications with standard http:// and https:// links (same as universal links on iOS.)