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 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.
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.
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.
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.
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"
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.