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