Celsius' Notes
Hire me as a freelancer for your mobile (iOS native, cross-platform with React Native), web and backend software needs.
Go to portfolio/resumé
6 min read

Custom URL Schemes in iOS

URL schemes are essentially just specially formatted URLs linking to content within your app. You can use URL schemes in your iOS (and also macOS) apps to implement deep-linking and also to give third-party apps the ability to open your app.
You could, for instance, register a URL scheme and send out an email with a button or link to open your app.

However, apple strongly recommends using Universal Links instead of URL schemes as they are more secure, albeit not as quick and easy to implement as URL schemes.

Limitations

You can't just register any URL scheme, as some are reserved for system apps. Following is a list of such URL schemes: http, https, tel, sms, facetime, facetime-audio, mailto. You can, however, launch the applications that support these URL schemes from within your app. More on that later.

How to set up a URL Scheme for your app

First, come up with the format for your app's URLs. We'll go with celsiusapp://

Now, you should register your URL scheme. This will allow the system to redirect this URL scheme to your app. Do this by navigating to the Info tab in Xcode and scrolling down to "URL Types". Click the "+" to add a new URL type.

Enter "celsiusapp" in the URL Schemes input field and "com.celsiusnotes" in the identifier field. The identifier serves to distinguish your app from others that have registered support for the same scheme. It is recommended to use a reverse DNS string of a domain that you own. Note, that this does not prevent other apps from registering the same scheme with the same identifier - even if they don't actually own that domain - and handling the URLs associated with that scheme and identifier.

Select "Editor" in the Role dropdown. Editor is for schemes that your app defines, while Viewer is for schemes that your app does not define but can handle.

url_schemes-2

Go ahead and build and run your app. Open Safari in your simulator or on your device and type in "celsiusapp://" in the address bar and hit enter. You should be prompted by Safari, asking whether you would like to open that page in the app you just built.

screen-1

Handling URLs

As you might know, a URL consists of more parts than just the scheme. There is also the hostname, the path and the query (port number and fragment are not relevant here). When your app is opened through a URL scheme you have registered for it, it can handle the URL string upon being opened.

If your has not opted into Scenes, you will have to implement the application(:open:options:) method in the AppDelegate.swift file to handle incoming URLs. If, on the other hand, your app has opted into using Scenes, as is the default for Xcode 11 and iOS 13, then you will have to implement the scene(_:willConnectTo:options:) and the scene(_:openURLContexts:) delegate methods in the SceneDelegate.swift class. The former delegate method is called when your app is launched, regardless of whether it was launched via a URL, while the latter will be called when your app is opened via a URL while running or after having been suspended in memory.

While opted into Scenes

We're going to write a function to handle incoming URLs for both the scene(_:willConnectTo:options:) and the scene(_:openURLContexts:) delegate methods.

The handleUrl() function below accepts a URL as its only parameter and dissects that URL into its constituent parts: host, path and params. The function will prematurely return, should any of those parts not exist.
Then the function simply prints out the host, all path components and the keys (names) and values for all query parameters. In a real app, instead of simply printing out those parts, you would of course take some action, like navigating to a certain screen in your app based on the path and showing certain items based on the query parameters.

    func handleURL(url: URL) {

        // A host, a path and query params are expected, else the URL will not be handled.
        guard let components = NSURLComponents(url: url, resolvingAgainstBaseURL: true),
            let host = components.host,
            let _ = components.path,
            let params = components.queryItems else {
                NSLog("Invalid URL. Host, path and query params are expected")
                return
        }
        
        NSLog("host: \(host)")
        
        for (index, pathComponent) in url.pathComponents.enumerated() {
            NSLog("pathComponent \(index): \(pathComponent)")
        }
        
        for query in params {
            if let value = query.value {
                NSLog("Query param \(query.name): \(value)")
                continue
            }
            
            NSLog("Query param \(query.name) has no value")
        }
    }

To handle incoming URLs we simply call this function in both the scene(_:willConnectTo:options:) and the scene(_:openURLContexts:) delegate methods:

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let _ = (scene as? UIWindowScene) else { return }
        
        
        // Since this function isn't exclusively called to handle URLs we're not going to prematurely return if no URL is present.
        if let url = connectionOptions.urlContexts.first?.url {
            handleURL(url: url)
        }
    }
    // This delegate method is called when the app is asked to open one ore more URLs.
    // We're only going to handle one URL in this post. Handling, multiple URLs, however, is simply a matter of applying
    // the code to all other URLs in the `URLContexts` set.
    func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
        // Get the first URL out of the URLContexts set. If it does not exist, abort handling the passed URLs and exit this method.
        guard let url = URLContexts.first?.url else {
            return NSLog("No URL passed to open the app")
        }
        
        handleURL(url: url)
    }

Go ahead and build your app. Then, without closing your app, open the Safari app and type in celsiusapp://hostpart/my/path/?name=celsius&color=blue into the address bar and hit go. After you now tap on open, you should see the following output in the Xcode console:

host: hostpart
pathComponent 0: /
pathComponent 1: my
pathComponent 2: path
Query param name: celsius
Query param color: blue

Congrats, you have successfully handled an incoming URL!

This will also work when your app is launched after having been terminated. You just won't be able to see the output in the Xcode console, as your app disconnects from the Xcode debugger when it's closed. Open up the Console app by typing "Console" into the Spotlight search. Make sure your app is terminated. In the Console application, select your simulator or device and click on "Clear" to clear previous logs. Then open Safari in your simulator or on your device and type in celsiusapp://hostpart/my/path/?name=celsius&color=blue again and hit go. If you now filter for "PROCESS" in the Console application and type in the name of your application, you should see the above host, path components etc. printed to the logs.

While opted out of Scenes

If your app has not opted into scenes, as is the case for apps that support iOS versions < 13, you need to handle the incoming URL in the application(_:open:options:) method in AppDelegate.swift

We can use the same handleURL function as we used above. So the only thing we need to do is implement the delegate function and pass the URL to the handleURL function.

    func application(_ application: UIApplication,
                     open url: URL,
                     options: [UIApplication.OpenURLOptionsKey : Any] = [:] ) -> Bool {
        
        self.handleURL(url: url)
        return true
    }   

That's all. You can get more information about the URL from the options object, which we are not going to do at this point, however.

You can try opening the app from a URL in Safari again as we did before. It should log the same output as it did above. Unlike when working with Scenes as shown above, we can handle URLs both when the app was launched from a terminated state and when it was opened from the background or a suspended state with the same method.

LAUNCHING APPS WITH URL SCHEME

You can also open an app via a URL programmatically. Use the open(_:options:completionHandler:) method of UIApplication to do that.
Use the completion handler to be notified when the URL has been delivered successfully.

let url = URL(string: "celsiusapp://hostpart/my/path/?name=celsius&color=blue")
       
UIApplication.shared.open(url!) { (result) in
    if result {
       // The URL was delivered successfully!
    }
}

Note, if you want to try this out, you'd have to do it from another app rather than the one we have been working with in this article.

Alternative to URL Schemes

I have already mentioned Universal Links before in this article. Universal links are a more secure and sophisticated alternative to URL schemes. Apple recommends using Universal Links whenever possible.
If you do use URL schemes, make sure to validate URLs and limit the available actions incoming URL schemes may trigger. For instance, you should probably not allow incoming URLs to trigger destructive or mutating actions on the user's data.