iOS: How to open Deep Links, Notifications and Shortcuts

One tool to rule them all

Have you ever built an app, that handles Push Notifications? If your app is something more complex than a “Hello, World!” app, then, most likely, the answer is yes.

How about opening Shortcuts? When all the new iOS devices support 3d touch, this feature is becoming something more than just a nice-to-have addition.

Does your app support the Universal Links? This is a relatively new feature, that is becoming popular in the modern apps. So, if you don’t use it, you may want to start.

The good thing is that you can easily find dozens of learning materials, examples, and tutorials on each if these three topics.

But what is your app has to support all of these features? Do you really have to implement it as three separate components?


Before continue, let’s clarify the terms:

  1. Universal Links is the way to intercept some of the URL’s and instead of handling the URL in Safari, open a specific app page. Universal links require some backend work, so we will stick to Deep Links in this tutorial. Deep Links act the similar way, but it handles the custom URL schemes. Its implementation is not much different, so it won’t be difficult to add a Universal Links support if you need it.
  2. Shortcut is the way to launch on the specific page based on selected shortcut item when you force touch on the app icon. This feature requires the 3d-touch enabled device.
  3. Notifications: when you tap the notification (either remote or local), the app will be launched on a specific page or do certain actions.

From this descriptions you can see, that all these three features are just the different types of the same essence: when the app handles any of them, it launching from the certain page.

Apple called this a Launching Options, that are handled by AppDelegate didFinishLaunchingWithOptions method.

However, when it comes to implementation of all launching options, it becomes confusing, and often leads to hundreds of lines of redundant code. Implementation becomes even more complicated, when the app is not launching, but entering a foreground mode. Shortcut handling, deep linking, and notification handling is happening in different delegate methods, and seem to have nothing in common.

To make if clear, I will state it once again: all these features are serving the same purpose — opening a specific app page.

The question is, how to make it all work together in a nice and clean way.


Project Setup

Before we start implementing any of launching options, create a list of pages that you want to access. Let’s say, we are building an Apartment bookings app and we want to have a quick access to the following parts:

  • My messages page (preview): accessible via shortcut
  • Messages (specific chat): accessible via push notification
  • Create new listing: accessible via shortcut for the host profile only
  • My Activity: accessible via shortcut
  • Booking request: accessible both via email link (deep link) and push notification

First, create a simple project with a ViewController, Navigation title and s Switch Profile Button:

Project Setup

View Controller will have a current profile type and a mechanism to switch between profiles.

I am not using any software design patterns because this is not the part of this tutorial. In the real app, you will need to find a better structure, than just storing a ProfileType right in the ViewController.
enum ProfileType: String {
case guest = "Guest" // default
case host = "Host"
}
class ViewController: UIViewController {
var currentProfile = ProfileType.guest
   override func viewDidLoad() {
super.viewDidLoad()
configureFor(profileType: currentProfile)
}
   @IBAction func didPressSwitchProfile(_ sender: Any) {
currentProfile = currentProfile == .guest ? .host : .guest
configureFor(profileType: currentProfile)
}
   func configureFor(profileType: ProfileType) {
title = profileType.rawValue
}
}

A single tool to rule them all

To straighten the purpose of this article, when we open a specific app page, I will call it a Deep Link, no matter what mechanism we are using.

Now, when we have a basic structure and UI, let’s organize our list of Deep Link elements:

enum DeeplinkType {
   enum Messages {
case root
case details(id: String)
}
   case messages(Messages)
case activity
case newListing
case request(id: String)
}
In this tutorial, I will not cover the swift enum theory. You can read here, if you need a clarification on how to use nested enums and enums with associated values.

Next, we can create a singleton manager, that will have all the Deep Linking options:

let Deeplinker = DeepLinkManager()
class DeepLinkManager {
fileprivate init() {}
}

Add an optional property, that will hold the current DeeplinkType:

let Deeplinker = DeepLinkManager()
class DeepLinkManager {
fileprivate init() {}
   private var deeplinkType: DeeplinkType?
}

Based on this deeplinkType the app will decide, what page it has to open:

let Deeplinker = DeepLinkManager()
class DeepLinkManager {
fileprivate init() {}
   private var deeplinkType: DeeplinkType?
   // check existing deepling and perform action
func checkDeepLink() {
   }
}

Whether the app is launching or entering the foreground mode, it will call didBecomeActive appDelegate method. That’s where we will check for any existing deep links that we can handle:

func applicationDidBecomeActive(_ application: UIApplication) {
   // handle any deeplink
Deeplinker.checkDeepLink()
}

Every time when the app become active, it will check if there is a deep link to open. Let’s create a navigator class, that will open a specific app page based on the DeeplinkType:

class DeeplinkNavigator {
static let shared = DeeplinkNavigator()
private init() { }

func proceedToDeeplink(_ type: DeeplinkType) {
   }
}

It this tutorial, we will simply display an alert with the name of the DeeplinkType we handled:

private var alertController = UIAlertController()
private func displayAlert(title: String) {
alertController = UIAlertController(title: title, message: nil, preferredStyle: .alert)
let okButton = UIAlertAction(title: "Ok", style: .default, handler: nil)
alertController.addAction(okButton)
   if let vc = UIApplication.shared.keyWindow?.rootViewController {  
if vc.presentedViewController != nil {
alertController.dismiss(animated: false, completion: {
vc.present(self.alertController, animated: true, completion: nil)
})
} else {
vc.present(alertController, animated: true, completion: nil)
}
}
}

In proceedToDeeplink we switch between DeeplinkTypes and decide, what alert to display:

func proceedToDeeplink(_ type: DeeplinkType) {
switch type {
case .activity:
displayAlert(title: "Activity")
case .messages(.root):
displayAlert(title: "Messages Root")
case .messages(.details(id: let id)):
displayAlert(title: "Messages Details \(id)")
case .newListing:
displayAlert(title: "New Listing")
case .request(id: let id):
displayAlert(title: "Request Details \(id)")
}
}

Return to the DeepLinkManager and use the navigator to handle deepLink:

// check existing deepling and perform action
func checkDeepLink() {
guard let deeplinkType = deeplinkType else {
return
}

DeeplinkNavigator().proceedToDeeplink(deeplinkType)
   // reset deeplink after handling
self.deeplinkType = nil // (1)
}
Don’t forget to reset the deepLink to nil (1) after you use it. Otherwise, the same link will be handled again when you open the app next time.

Now we only need to check if there are any deep links (Shortcuts, Deep Links or Notifications) have to be handled, parse them to DeeplinkType and give them the DeepLinkManager.

First, we have to do a basic setup for Shortcuts, Deep Links, and Notifications.

While we want a DeepLinkManager to handle any kind of deep links, remember about SRP (single responsibility principle): we will not mix together the ways we parse different types of deep links.


Shortcuts

The common-used way of creating the static Shortcuts is using the info.plist file. Here is a brief tutorial, if you want to check it out.

In our case, some of the shortcuts will be dynamic. So, we will do everything in code. This method gives you more flexibility and control, and it’s actually much easier to understand.

First, we create a ShortcutParser. This class will be responsible solely for the shortcuts.

class ShortcutParser {
static let shared = ShortcutParser()
private init() { }
}

Then we need to define the possible shortcut keys:

enum ShortcutKey: String {
case newListing = "com.myApp.newListing"
case activity = "com.myApp.activity"
case messages = "com.MyApp.messages"
}

In ShortcutParser we create registerShortcutsFor method, that will register the shortcuts for the different user types.

func registerShortcuts(for profileType: ProfileType) {
   let activityIcon = UIApplicationShortcutIcon(templateImageName: "Alert Icon")
let activityShortcutItem = UIApplicationShortcutItem(type: ShortcutKey.activity.rawValue, localizedTitle: "Recent Activity", localizedSubtitle: nil, icon: activityIcon, userInfo: nil)
   let messageIcon = UIApplicationShortcutIcon(templateImageName: "Messenger Icon")
let messageShortcutItem = UIApplicationShortcutItem(type: ShortcutKey.messages.rawValue, localizedTitle: "Messages", localizedSubtitle: nil, icon: messageIcon, userInfo: nil)
   UIApplication.shared.shortcutItems = [activityShortcutItem, messageShortcutItem]
switch profileType {
case .host:
let newListingIcon = UIApplicationShortcutIcon(templateImageName: "New Listing Icon")
let newListingShortcutItem = UIApplicationShortcutItem(type: ShortcutKey.newListing.rawValue, localizedTitle: "New Listing", localizedSubtitle: nil, icon: newListingIcon, userInfo: nil)
UIApplication.shared.shortcutItems?.append(newListingShortcutItem)
case .guest:
break
}
}

We create activityShortcutItem and messageShortcutItem for both profile types. If the current user is a Host, we add a newListingShortcutItem. For each shortcut, we use a UIApplicationShortcutIcon (this icon will be displayed next to the shortcut title when you force-touch the app icon).

We will call this method from our ViewController: when the user switches the profile, the shortcuts will be reconfigured based on the new profile type:

func configureFor(profileType: ProfileType) {
title = profileType.rawValue
ShortcutParser.registerShortcuts(for: profileType)
}
For the static shortcuts, it’s better to set the shortcuts from the appDelegate, so the app doesn’t have to load the ViewController before it sets the shortcuts

Build and run the app and test its behavior:

  1. Force-touch on the app icon to see shortcuts options
  2. Switch the profile
  3. Force-touch on the app icon again to see another set of shortcuts options

However, if you tap on any of the shortcuts, it will just open the app on the default page. This is because we only added the shortcuts to the app, but we haven’t yet told the app how to handle them.

Switch to the AppDelegate and add the performActionForShortcutItem delegate method:

// MARK: Shortcuts
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
}

This method will detect when the shortcut is triggered, and the completionHandler will tell the delegate whether to handle it or not. Again, we will not place any shortcut logic in appDelegate. Instead, we create a method in DeeplinkManager:

@discardableResult
func handleShortcut(item: UIApplicationShortcutItem) -> Bool {
deeplinkType = ... // we will parse the item here
return deeplinkType != nil
}

This method will first try to parse the shortcut item to the DeeplinkType, and then it will return the boolean value, indicating if the parsing succeeded or not. At the same time, it will save the parsed shortcut in the deeplinkType variable.

@discardableResult tells the compiler to ignore the result value if we don't use it, so we don't have an “unused result” warning

Return to appDelegate and complete performActionFor method:

// MARK: Shortcuts
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
completionHandler(Deeplinker.handleShortcut(item: shortcutItem))
}

The last thing we need to do is to parse the shortcut item to the DeeplinkType. We already have a ShortcutParser class, that is responsible for all shortcuts-related actions. Add another method:

func handleShortcut(_ shortcut: UIApplicationShortcutItem) -> DeeplinkType? {
switch shortcut.type {
case ShortcutKey.activity.rawValue:
return .activity
case ShortcutKey.messages.rawValue:
return .messages(.root)
case ShortcutKey.newListing.rawValue:
return .newListing
default:
return nil
}
}

Now, return to the DeeplinkManager and complete handleShortcut method:

@discardableResult
func handleShortcut(item: UIApplicationShortcutItem) -> Bool {
deeplinkType = ShortcutParser.shared.handleShortcut(item)
return deeplinkType != nil
}

That’s all the setup we need to handle the Shortcuts! Let’s go through it step-by-step once again. When we tap on the shortcut icon:

  1. The shortcut action triggers the appDelegate performActionForShortcutItem method
  2. performActionForShortcutItem method passes the ShortcutItem to DeeplinkManager
  3. DeeplinkManager tries to parse the ShortcutItem to DeeplinkType using the ShortcutParser
  4. On applicationDidBecomeActive we perform the check for any DeeplinkTypes
  5. If DeeplinkType exists (this means, step 3 succeed), we do appropriate action using DeeplinkNavigator
  6. Once the shortcut is handled, the DeeplinkManager resets the current shortcut item to nil, so it won’t be used again

Run the app and test how it works in 2 scenarios: when the app launches from the closed state, and when the app is resuming from the background mode. In both cases, you should see the alert with an appropriate message:


Deep Links

The Deeplinks we are going to handle will have the following format:

deeplinkTutorial://messages/1
deeplinkTutorial://request/1

Copy and save this URLs in “Notes” on your testing device. If you tap on any of the links, nothing is going to happen.

If this was a Universal Link, tapping on the link would open a browser with a link URL.

When we finish with this section, tapping the URL will open the appropriate page in our app.

AppDelegate will detect if the app was opened with deeplink URL and trigger the openUrl method:

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

}

Return value tells the delegate whether to open the URL or not.

If you want to support the Universal Links (introduced in iOS9), also add the following delegate method:

// MARK: Universal Links
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool {
if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
if let url = userActivity.webpageURL {

}
}
return false
}
ContunueUserActivity is also triggered when you launch the app through the Spotlighs items (not covered in this tutorial).

Following the same pattern that we used for the Shortcuts, we create a DeeplinkParser:

class DeeplinkParser {
static let shared = DeeplinkParser()
private init() { }
}

To parse the Deeplink, we create a method that takes an URL and returns the optional DeeplinkType:

func parseDeepLink(_ url: URL) -> DeeplinkType? {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true), let host = components.host else {
return nil
}
   var pathComponents = components.path.components(separatedBy: "/")
   // the first component is empty
pathComponents.removeFirst()
   switch host {
case "messages":
if let messageId = pathComponents.first {
return DeeplinkType.messages(.details(id: messageId))
}
case "request":
if let requestId = pathComponents.first {
return DeeplinkType.request(id: requestId)
}
default:
break
}
return nil
}
Note, that this parsing method will depend on your deeplinks structure, and my solution is only an example.

Now we can connect this parser to our main deeplink class. Add this method in the DeeplinkManager:

@discardableResult
func handleDeeplink(url: URL) -> Bool {
deeplinkType = DeeplinkParser.shared.parseDeepLink(url)
return deeplinkType != nil
}

In appDelegate we complete the openUrl and continueUserActivity methods:

// MARK: Deeplinks
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
return Deeplinker.handleDeeplink(url: url)
}
// MARK: Universal Links
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool {
if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
if let url = userActivity.webpageURL {
return Deeplinker.handleDeeplink(url: url)
}
}
return false
}

There is one more thing we need to do: tell our app what kind of link it should detect. Add this code snippet to the info.plist file (right click on info.plist -> Open As -> Source Code):

<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.deeplinkTut.Deeplink</string>
<key>CFBundleURLSchemes</key>
<array>
<string>deeplinkTutorial</string>
</array>
</dict>
</array>
Make sure you don’t break the XML file structure.

Once you added this code, try to open the plist as a Property List. You should see the following lines:

This will tell the app to only detect the links with the URL “deeplinkTutorial”.

Let’s go through all the steps again:

  1. User taps on the deeplink outside of the app
  2. AppDelegate detects the link and triggers openUrl delegate method (or ContinueUserActivity delegate method for the Universal Links)
  3. openUrl method passes the link to Deeplink Manager
  4. Deeplink Manager tries to parse the link to Deeplink Type using the DeeplinkParser
  5. On applicationDidBecomeActive we perform the check for any DeeplinkTypes
  6. If DeeplinkType exists (this means, step 4 succeed), we do appropriate action using DeeplinkNavigator
  7. Once the shortcut is handled, the DeeplinkManager resets the current shortcut item to nil, so it won’t be used again

Run the app, and try to open the link that you’ve saved in your notes:


Notifications

Configuring the project to support push notifications is not the part of this tutorial, but you can find the detailed examples here and here.

To send APNS notifications you can use a local server and PusherAPI (this is one of the simple ways).

We will only cover the part between tapping the push notification and seeing the result.

When the app is closed or running on the background more, tapping on the notification banner will trigger didReceiveRemoteNotification appDelegate method:

func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {

}
This method will also be triggered when the app received a push notification while it is running in the foreground mode. Because we only considering the scenarios when you want to open the app on the certain page, we will not cover handling notifications in the foreground mode.

To handle the Notifications we will create a NotificationParser:

class NotificationParser {
static let shared = NotificationParser()
private init() { }
   func handleNotification(_ userInfo: [AnyHashable : Any]) -> DeeplinkType? {
      return nil
}
}

Now we can connect this method to the Deeplink Manager:

func handleRemoteNotification(_ notification: [AnyHashable: Any]) {
deeplinkType = NotificationParser.shared.handleNotification(notification)
}

And complete the appDelegate didReceiveRemoteNotification method:

func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
Deeplinker.handleRemoteNotification(userInfo)
}

The last step is to finish the parsing method in the NotificationParser. This will depend on your notification structure, but the basic parsing technic will be similar:

func handleNotification(_ userInfo: [AnyHashable : Any]) -> DeeplinkType? {
if let data = userInfo["data"] as? [String: Any] {
if let messageId = data["messageId"] as? String {
return DeeplinkType.messages(.details(id: messageId))
}
}
return nil
}

If you configured the app to support the push notifications and want to test it, here is the notification I am using to deliver a message:

apns: {
aps: {
alert: {
title: "New Message!",
subtitle: "",
body: "Hello!"
},
"mutable-content": 0,
category: "pusher"
},
data: {
"messageId": "1"
}
}
I am using a local NodeJS server and Pusher API to send notifications in this tutorial. It only takes a few minutes to setup and requires the basic knowledge of NodeJS or some copy-pasting skills.

Run the app, take it to background mode and send a notification. Once you receive the notification, tap on it to open the app:

Here is what’s happening behind the scene:

  1. When you tap on the notification, the app triggers the didReceiveRemoteNotification delegate method
  2. didReceiveRemoteNotification passes the notification info to the Deeplink Manager
  3. Deeplink Manager tries to parse the Notification User Info to the Deeplink Type using NotificationParser
  4. On applicationDidBecomeActive we perform the check for any DeeplinkTypes
  5. If DeeplinkType exists (this means, step 3 succeed), we do appropriate action using DeeplinkNavigator
  6. Once the shortcut is handled, the DeeplinkManager resets the current shortcut item to nil, so it won’t be used again

Using this approach, you can easily add or modify any of the items without significant code changes. And what is more important, you can parse any deeplink using the appropriate parser. For example, to add “New Request” in Notification handler, you only need to modify handleNotification method in NotificationParser:

func handleNotification(_ userInfo: [AnyHashable : Any]) -> DeeplinkType? {
if let data = userInfo["data"] as? [String: Any] {
if let messageId = data["messageId"] as? String {
return DeeplinkType.messages(.details(id: messageId))
}
if let requestId = data["requestId"] as? String {
return DeeplinkType.request(.details(id: requestId))
}
}
return nil
}
Note, that we don’t use the didFinishLaunchingWithOptions for any of these deeplinks. Everything is handled by applicationDidBecomeActive.

Congratulations! Now your app has a universal support of Shortcuts, Deeplinks and Notifications!


Check out the final project here:

Thanks for reading! If you like this tutorial please hit 🖤.