Deep linking and URL scheme in iOS

Opening an app from an URL is such a powerful iOS feature. Its drives users to your app, and can create shortcuts to specific features. This week, we’ll dive into deep linking on iOS and how to create an URL scheme for your app.

When we talk about deep linking for mobile app, it means creating a specific URL to open a mobile application. It separate into two formats:

Today, we’ll focus on the former one.

I’ll mostly focus on the code for an UIKit implementation but I’ll also briefly cover SwiftUI one if that’s what you’re looking for too.

Setting up URL Scheme

Setting up an custom URL scheme for iOS is same regardless you are using SwiftUI or UIKit. In Xcode, under your project configuration, select your target and navigates to Info tab. You’ll see an URL Types section at the bottom.

deep-linking-url-scheme

Clicking + , I can create a new type. For the identifier, I often reuse the app bundle. For the URL Schemes, I would suggest to use the app name (or shortened) to be as short as possible. It shouldn’t include any custom character. For the example, I’ll use deeplink .

That’s it. The app is ready to recognize the new URL, now we need to handle it when we receive one.

SwiftUI deep linking.

If you don’t have any AppDelegate and SceneDelegate files, which is most of the case for SwiftUI implementation, we don’t have much work to do.

In the App implementation, we can capture the url open from onOpenURL(perform:) action.

import SwiftUI  @main struct DeeplinkSampleApp: App   var body: some Scene   WindowGroup   ContentView()  .onOpenURL < url in  print(url.absoluteString)  >  >  > > 

To test it, I can install the app on a simulator and launch the given url from the Terminal app

xcrun simctl openurl booted "deeplink://test" 

Pretty cool! Let’s look how UIKit implementation is different.

UIKit deep link

On paper, UIKit or SwiftUI shouldn’t make a difference in the way we handle deep linking. However, it mostly falls down to having an AppDelegate or SceneDelegate which are more common for UIKit apps.

For older apps that only have AppDelegate , the app captures the deeplink opening from the following method.

extension AppDelegate    func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any]) -> Bool    print(url.absolueString)  return true  > > 

The function return a Boolean if the app can handle that given url.

For newer apps that includes SceneDelegate , the callback will be there. It’s important to note that the AppDelegate won’t get called, even if you implement it.

extension SceneDelegate   func scene(_ scene: UIScene, openURLContexts URLContexts: SetUIOpenURLContext>)   guard let firstUrl = URLContexts.first?.url else   return  >   print(firstUrl.absoluteString)  > > 

In this implementation, we can notice we don’t need anymore to return any result. However, the parameter passed is now a Set<> and not just a URL anymore, it’s to open one or more URLs. I don’t have a use-case where we would have more than URL so I’ll just keep one for the moment.

The same way as earlier, we can install the app on our simulator and try to see if all is setup correctly. We should see print our deeplink URL.

xcrun simctl openurl booted "deeplink://test" 

Once it’s setup, the idea is to create routes to identify and open the right screen. Let’s dive in.

Deeplink handler implementations

The idea is pretty simple, for a given link, we need to identify what user journey or screen we should open. Since they can be many features across the app, and because we want to avoid a massive switch case to handle it, we’ll be smarter and divide to conquer.

For this example, let’s image we have a video editing app. They are 3 main tabs, to edit a new video, to list the videos edited, then an account page with different app and user information.

We can think of three main paths

First, I’ll create a protocol of deeplink handler to define the minimum requirements of any new handlers.

protocol DeeplinkHandlerProtocol   func canOpenURL(_ url: URL) -> Bool  func openURL(_ url: URL) > 

I will also define a DeeplinkCoordinator that will holds on the handlers and find the right one to use. It also returns a Boolean like the AppDelegate has, so we can use in different implementations.

 protocol DeeplinkCoordinatorProtocol   @discardableResult  func handleURL(_ url: URL) -> Bool >  final class DeeplinkCoordinator    let handlers: [DeeplinkHandlerProtocol]   init(handlers: [DeeplinkHandlerProtocol])   self.handlers = handlers  > >  extension DeeplinkCoordinator: DeeplinkCoordinatorProtocol    @discardableResult  func handleURL(_ url: URL) -> Bool  guard let handler = handlers.first(where: < $0.canOpenURL(url) >) else   return false  >   handler.openURL(url)  return true  > > 

Now we can define separate handlers, one for each different path. Let’s start first with the Account journey, the simplest one.

final class AccountDeeplinkHandler: DeeplinkHandlerProtocol    private weak var rootViewController: UIViewController?  init(rootViewController: UIViewController?)   self.rootViewController = rootViewController  >   // MARK: - DeeplinkHandlerProtocol   func canOpenURL(_ url: URL) -> Bool   return url.absoluteString == "deeplink://account"  >   func openURL(_ url: URL)   guard canOpenURL(url) else   return  >   // mock the navigation  let viewController = UIViewController()  viewController.title = "Account"  viewController.view.backgroundColor = .yellow  rootViewController?.present(viewController, animated: true)  > > 

To keep it simple, I only test for the matching url and navigate to the right screen. I also set a background color to see what is my landing. In your case, we can just set the right UIViewController rather than an empty one.

I will do the same for the different video journeys.

final class VideoDeeplinkHandler: DeeplinkHandlerProtocol    private weak var rootViewController: UIViewController?  init(rootViewController: UIViewController?)   self.rootViewController = rootViewController  >   // MARK: - DeeplinkHandlerProtocol   func canOpenURL(_ url: URL) -> Bool   return url.absoluteString.hasPrefix("deeplink://videos")  >  func openURL(_ url: URL)   guard canOpenURL(url) else   return  >   // mock the navigation  let viewController = UIViewController()  switch url.path   case "/new":  viewController.title = "Video Editing"  viewController.view.backgroundColor = .orange  default:  viewController.title = "Video Listing"  viewController.view.backgroundColor = .cyan  >   rootViewController?.present(viewController, animated: true)  > > 

Now we can inject them into the DeeplinkCoordinator and let it handle the right route. We’ll have two variations, the first one for AppDelegate .

class AppDelegate: UIResponder, UIApplicationDelegate    lazy var deeplinkCoordinator: DeeplinkCoordinatorProtocol =   return DeeplinkCoordinator(handlers: [  AccountDeeplinkHandler(rootViewController: self.rootViewController),  VideoDeeplinkHandler(rootViewController: self.rootViewController)  ])  >   var rootViewController: UIViewController?   return window?.rootViewController  >   // .   func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any]) -> Bool   return deeplinkCoordinator.handleURL(url)  > > 

And the second one for the SceneDelegate

class SceneDelegate: UIResponder, UIWindowSceneDelegate    lazy var deeplinkCoordinator: DeeplinkCoordinatorProtocol =   return DeeplinkCoordinator(handlers: [  AccountDeeplinkHandler(rootViewController: self.rootViewController),  VideoDeeplinkHandler(rootViewController: self.rootViewController)  ])  >()   var rootViewController: UIViewController?   return window?.rootViewController  >   // .   func scene(_ scene: UIScene, openURLContexts URLContexts: SetUIOpenURLContext>)   guard let firstUrl = URLContexts.first?.url else   return  >   deeplinkCoordinator.handleURL(firstUrl)  > 

We can test it again the same way we did so far, hoping to land on the right screen (expecting orange background).

xcrun simctl openurl booted "deeplink://videos/new" 

deep-linking-ios

To summarize, once the URL scheme was setup, we defined a funnel to capture all the deep links used to open the app and leveraged protocol oriented programming to create multiple implementations of handlers, one for each specific path.

This implementation is extensible for newer path and can easily be unit tested to make sure each parts behaves as expected.

That being said, there could be few improvements, like verifying full path rather than relative one, for safer behavior. The navigation only present but it’s to focus on the handler and not the transition itself.

On a security note, if you also pass parameters within your deeplink, make sure to verify the type and values expected. It could expose different injection vulnerabilities if we’re not careful.

From there, you should have a good understanding of how to use and handle deeplink to open your app and jump to a specific screen. This code is available on Github.