Skip to content

UIKit Deeplinking Solutions

Overview

Make sure you've gone through the Getting Started document that fits your app.

If you have a SwiftUI app, you'll want to read the SwiftUI Deeplinking Example article instead.

Simple solution:

The simple solution is to just present a view controller from the key window's root view controller:

AppDelegate.swift
MIClient.start(launchOptions: launchOptions) { result in
  var window: UIWindow?

  if #available(iOS 13, *) {
    window = UIApplication.shared.windows.first(where: \.isKeyWindow)
  } else {
    window = UIApplication.shared.keyWindow?.window
  }

  switch result {
  case let .success(url):
    // note that the URL here is not the MI Link, but rather your clickthrough link
    switch url {
    case "https://example.com/products":
      window?.rootViewController?.present(ProductViewController(), animated: true, completion: nil)
    default: 
      break
    }

  case let .failure(.failure(url, message)):
    debugPrint(url, message)

  case .failure:
    debugPrint("Some unknown error occurred while resolving a deeplink")
  }
}

More complicated solution:

The more complicated solution would allow you to show the required controller in the proper state such as if you had a tab bar controller, you'd want to navigate to the tab and have that tab open the correct screen.

In the following example, we delegate the routing to a DeeplinkManager, which matches a given URL with a series of regexes. If a match is found, the content, such as the product id or the category name, is extracted, and published using the Combine framework. This allows subscribers, CatalogCoordinator, to subscribe to this event, and handle it.

AppDelegate.swift
MIClient.start(launchOptions: launchOptions) { result in
  switch result {
  case let .success(url):
    DeeplinkManager.route(to: url)

  case let .failure(.failure(url, message)):
    debugPrint(url, message)

  case .failure:
    debugPrint("Some unknown error occurred while resolving a deeplink")
  }
}
DeeplinkManager.swift
import Foundation

/// The url paths that our app can handle 
enum DeeplinkPath: String, CaseIterable {
  case product = "\\b/product/\\w+/?" // ends with /product/:id
  case category = "\\b/category/\\w+/?" // ends with /category/:category
  case subCategory = "\\b/category/\\w+/(\\w|-)+/?" // ends with /category/:category/:subcategory

  /// coerces a url into a DeeplinkPath
  static func path(url: URL) -> DeeplinkPath? {
    let stringValue = url.absoluteString

    return DeeplinkPath.allCases.first {
      stringValue.contains($0.rawValue) ||
      (stringValue.range(of: $0.rawValue, options: .regularExpression) != nil)
    }
  }
}

/// A representation of the page and any information that may be needed for that page
enum Deeplink {
  case product(id: String)
  case category(gender: Product.Gender)
  case products(category: Product.Category)
}

/// Manages any incoming deeplinks and publishes an event if a url can be handled by the app. 
class DeeplinkManager {
  static let shared = DeeplinkManager()

  /// The deeplink that needs to be handled.
  ///
  /// Create a sink in places where you'd perform the navigation to a given controller.
  @Published private(set) var link: Deeplink?

  private init() {}

  /// Publishes a Deeplink that parts of the app subscribes to which will open the corresponding screen for a given link
  ///
  /// - Parameter url: The URL that opened the app
  /// - Returns: True if we were able to handle the link, otherwise false
  @discardableResult static func route(to url: URL) -> Bool {
    switch DeeplinkPath.path(url: url) {
    case .product:
      // Grabs the ID parameter
      guard let id = url.pathComponents.last else {
        return false
      }

      shared.link = .product(id: id)

      return true

    case .category:
      // Grabs the gender parameter
      guard let id = url.pathComponents.last,
            let gender = Product.Gender.from(deeplinkID: id)
      else {
        return false
      }

      shared.link = .category(gender: gender)

      return true

    case .subCategory:
      // Grabs the subcategory parameter
      guard let id = url.pathComponents.last,
            let category = Product.Category.from(deeplinkID: id)
      else {
        return false
      }

      shared.link = .products(category: category)

      return true

    case .none:
      return false
    }
  }
}
CatalogCoordinator.swift
import UIKit
import Combine

final class CatalogCoordinator: NSObject, Coordinator {
  var children: [Coordinator] = []

  var subscriptions = Set<AnyCancellable>()

  var navigationController: UINavigationController

  init(navigationController: UINavigationController) {
    self.navigationController = navigationController
  }

  func start() {
    let controller = CatalogViewController(section: .genders)
    controller.coordinator = self
    navigationController.setViewControllers([controller], animated: false)

    // Start a sink on any deeplinks that we might be able to manage
    DeeplinkManager.shared.$link
      .receive(on: DispatchQueue.main)
      .sink { [weak self] deeplink in
        switch deeplink {
        case let .category(gender):
          self?.showCategories(for: gender)

        case let .products(category):
          self?.showProducts(in: category)

        case let .product(id):
          guard let product = Product.mock.first(where: { $0.id == id }) else { return }

          self?.showProduct(product)

        default: break
        }
      }.store(in: &subscriptions)
  }

  ...

}