iOS Deeplink Handling: The Asynchronous Way

Mohsin Khan
Swiftable
Published in
5 min readAug 6, 2023

A practical guide on how to handle deeplinks in your iOS apps.

Deeplink Handling

In this article, you will learn about:

  • How to parse deeplink data?
  • How to manage deeplink data?
  • How to handle a deeplink?
  • How to handle a deeplink asynchronously?

Let’s explore these point step by steps.

Introduction

In modern iOS applications, handling deep links has become a crucial aspect of user engagement and retention. Deep links allow users to navigate directly to specific content within the app, enhancing the overall user experience. However, handling deep links asynchronously and distributing the responsibility among different screens and view models is a unique approach that can bring significant benefits. In this article, we’ll explore a novel solution for deep link handling in iOS with the MVVM (Model-View-ViewModel) app architecture, enabling seamless navigation and asynchronous API calls.

Pre-requisites:

  • Basic understanding of Deeplinks in iOS
  • MVVM (Model-View-ViewModel) app architecture

Deeplink Data Parsing

Firstly, the deeplink needs to be parsed and converted to an object that the rest of the module can understand. DeeplinkDataParserProtocol, is responsible for parsing the incoming deeplink URL and extracting relevant data. The DeeplinkDefaultDataParser class implements this protocol and validates the scheme of the URL. Upon successful validation, it extracts the query parameters and constructs a DeeplinkParsedData object.

enum DeeplinkQueryDataKey: String {
case type
}

struct DeeplinkParsedData {
let navigationType: DeeplinkNavigationType
let queryData: [DeeplinkQueryDataKey: Any]
}

protocol DeeplinkDataParserProtocol {
func parseDeeplink(_ urlString: String) -> DeeplinkParsedData?
}

final class DeeplinkDefaultDataParser: DeeplinkDataParserProtocol {
private func validateScheme(_ scheme: String) -> Bool {
// Validate the scheme here
}

func parseDeeplink(_ urlString: String) -> DeeplinkParsedData? {
// Consider an example deeplink URL as "sample-app://sampleapp?type=homeScreen&data=1234"
// After parsing the navigation type will be "homeScreen" in the DeeplinkParsedData object.
// Rest of the URL params will be part of query data in the DeeplinkParsedData.
guard let url = URL(string: urlString),
let scheme = url.scheme,
validateScheme(scheme)) else {
return nil
}

// Parse the URL components to extract query parameters
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)

// Create a dictionary to store the query data
var queryData = [DeeplinkQueryDataKey: Any]()
if let queryItems = components?.queryItems {
// Iterate through the query items and store them in the dictionary
queryItems.forEach({
if let value = $0.value,
let queryDataKey = DeeplinkQueryDataKey(rawValue: $0.name) {
queryData[queryDataKey] = value
}
})
}

// Extract the deeplink navigation type from the query data
if let navigationTypeValue = queryData[DeeplinkQueryDataKey.type] as? String,
let navigationType = DeeplinkNavigationType(rawValue: navigationTypeValue) {
return DeeplinkParsedData(navigationType: navigationType, queryData: queryData)
}

return nil
}
}

Deeplink Data Management

To facilitate communication between different view models and ensure a smooth flow of deep link data, the DeeplinkDataManager singleton is introduced. It stores the active deep link data and allows access to it from any view model that implements the ViewModelDeeplinkHandlerProtocol.

final class DeeplinkDataManager {
static let shared = DeeplinkDataManager()
private var activeDeeplinkData: DeeplinkParsedData?

private init() {}

func getDeeplinkData() -> DeeplinkParsedData? {
return activeDeeplinkData
}

func setDeeplinkData(_ data: DeeplinkParsedData) {
activeDeeplinkData = data
}

func removeDeeplinkData() {
activeDeeplinkData = nil
}
}

Deeplink Handling

The ViewModelDeeplinkHandlerProtocol defines the contract for any view model that wishes to support deep link handling. It includes the following key components:

  1. deeplinkState: An observable property that reflects the current state of the deep link handling process.
  2. supportedDeeplinkTypes: An array of DeeplinkNavigationType that indicates the types of deep links the view model can handle.
  3. finalDeeplinkTypes: An array of DeeplinkNavigationType representing the deep link types that can be marked as completed and removed from the DeeplinkDataManager.
  4. handleDeeplink(with data: DeeplinkParsedData, completionHandler: DeeplinkDataCompletionHandler?): A method that customizes the implementation of deep link handling for each view model.
enum DeeplinkNavigationType: String {
case homeScreen
case transactionScreen
}

enum ViewModelDeeplinkState {
case pending
case inProgress
case completed
case notActive
}

typealias DeeplinkDataCompletionHandler = () -> ()

protocol ViewModelDeeplinkHandlerProtocol: AnyObject {
var deeplinkState: Observable<ViewModelDeeplinkState> { get }
var supportedDeeplinkTypes: [DeeplinkNavigationType] { get }
var finalDeeplinkTypes: [DeeplinkNavigationType] { get }
func handleDeeplink(with data: DeeplinkParsedData, completionHandler: DeeplinkDataCompletionHandler?)
}

extension ViewModelDeeplinkHandlerProtocol {
func getActiveDeeplinkData() -> DeeplinkParsedData? {
return DeeplinkDataManager.shared.getDeeplinkData()
}

func handleAnyActiveDeeplink() {
// Set the initial deeplink state to 'notActive'
deeplinkState.value = .notActive

// Get the active deeplink data from the DeeplinkDataManager
guard let deeplinkData = getActiveDeeplinkData(),
// Check if the supportedDeeplinkTypes contains the current deeplink's navigation type
supportedDeeplinkTypes.contains(deeplinkData.navigationType) else {
return
}

// If the current deeplink's navigation type is one of the finalDeeplinkTypes, remove it from the DeeplinkDataManager
if finalDeeplinkTypes.contains(deeplinkData.navigationType) {
DeeplinkDataManager.shared.removeDeeplinkData()
}

// Set the deeplink state to 'inProgress'
deeplinkState.value = .inProgress

// Call the handleDeeplink which will be implemented in the respective View Models.
handleDeeplink(with: deeplinkData) { [weak self] in
// Once the handling is complete, update the deeplink state to 'completed'
guard let self else {
// Ensure self is not deallocated before updating the deeplink state
return
}
self.deeplinkState.value = .completed
}
}
}

Asynchronous Handling and Navigation

The true novelty of this solution lies in its ability to handle deep links asynchronously. When a view controller calls handleAnyActiveDeeplink() after viewDidLoad or viewDidAppear, the method checks for an active deep link and delegates the handling process to the corresponding view model.

The view model then verifies if it supports the received deep link type and starts the asynchronous handling process. In case the deep link type is one of the finalDeeplinkTypes, it removes the deep link data from the manager after completion. This ensures that once the deep link is fully processed, it won't interfere with subsequent navigation.

final class HomepageViewModel: ViewModelDeeplinkHandlerProtocol {
var supportedDeeplinkTypes: [DeeplinkNavigationType] {
return [
// Both homeScreen and transactionScreen Navigation types will be handled by this view model but only
// homeScreen will be marked complete. transactionScreen type will be handled by a different View Model.
.homeScreen,
.transactionScreen
]
}

var finalDeeplinkTypes: [DeeplinkNavigationType] {
return [
// Only homeScreen Navigation type will be marked as complete
.homeScreen
]
}

func handleDeeplink(with data: DeeplinkParsedData, completionHandler: DeeplinkDataCompletionHandler?) {
let type = data.navigationType
switch type {
case .homeScreen, .transactionScreen:
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
// Execute any async task and don't forget to call the completionHandler
completionHandler?()
}
default:
break
}
}
}

This is just my attempt to design the deeplink navigation architecture to support async handling.

Please feel free to comment any suggestions!!
You can reach me out at k.mohsin11@hotmail.com

--

--