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:
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.
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")
}
}
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
}
}
}
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)
}
...
}