SwiftUI Deeplinking Solutions
Overview
Make sure you've gone through the Getting Started document that fits your app.
If you have a UIKit app, you'll want to read the UIKit Deeplinking Example article instead.
This is just an example of how you might approach deeplinking in a SwiftUI app. You should utilize what works best for your app.
For Creative Tags links, you'll need to utilize the MIClient
to coerce the url into the clickthrough URL.
We'll manage Deeplinks with the Environment
. First, create the types to define what URLs your app can handle and a manager to coerce the URL into a Deeplink that your app can understand. In our example, we're looking for products, category, and sub-category urls.
import Foundation
import SwiftUI
/// 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
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.
/// This is the type that will be available in the environment.
enum Deeplink: Hashable {
case product(Product)
case category(gender: Product.Gender)
case products(category: Product.Category)
}
/// Manages any incoming deeplinks and coerces it into a Deeplink that the app can then handle.
struct DeeplinkManager {
/// Returns a Deeplink that should be navigated to.
///
/// - Parameter url: The URL that opened the app
/// - Returns: The Deeplink
static func route(to url: URL) -> Deeplink? {
switch DeeplinkPath.path(url: url) {
case .product:
// Grabs the ID parameter
guard let id = url.pathComponents.last else { return nil }
guard let product = Product.mock.first(where: { $0.id == id }) else { return nil }
return .product(product)
case .category:
// Grabs the gender parameter
guard let id = url.pathComponents.last,
let gender = Product.Gender.from(deeplinkID: id)
else {
return nil
}
return .category(gender: gender)
case .subCategory:
// Grabs the subcategory parameter
guard let id = url.pathComponents.last,
let category = Product.Category.from(deeplinkID: id)
else {
return nil
}
return .products(category: category)
case .none:
return nil
}
}
}
// MARK: - Define a new environment value property
// Allows us to store a Deeplink in the environment
struct DeeplinkKey: EnvironmentKey {
static var defaultValue: Deeplink? {
return nil
}
}
extension EnvironmentValues {
var deeplink: Deeplink? {
get {
self[DeeplinkKey.self]
}
set {
self[DeeplinkKey.self] = newValue
}
}
}
Once we've created our manager, we can then update our App to include Deeplink as state, and pass that state into our ContentView as an environment.
import SwiftUI
import MovableInk
@main
struct InkredibleRetailApp: App {
// Store the deeplink as state
@State var deeplink: Deeplink?
init() {
MIClient.start { _ in }
}
var body: some Scene {
WindowGroup {
ContentView()
// Share the deeplink state with the environment
.environment(\.deeplink, deeplink)
// handle universal links
.onOpenURL { url in
// Try to handle the link yourself
if let deeplink = DeeplinkManager.route(to: url) {
self.deeplink = deeplink
}
// and if you can't, ask the MIClient to try to handle it.
// If the MovableInk SDK can handle it, the closure above in `MIClient.start` will be called with
// the clickthrough link
else if MIClient.handleUniversalLink(url: url) {
return
}
}
.onReceive(MIClient.storedDeeplinkSubject.receive(on: DispatchQueue.main)) { value in
guard let value = value, let url = URL(string: value) else { return }
// Update our state by asking the DeeplinkManager to coerce the url to a deeplink
self.deeplink = DeeplinkManager.route(to: url)
}
}
}
}
Now in your views, you can observe the change of the deeplink in the environment and navigate.
struct ContentView: View {
@Environment(\.deeplink) var deeplink
var body: some View {
Text("Hello World")
.onChange(of: deeplink) { link in
// Here you can switch on the deeplink and set your state or navigate to where you need
}
}
}
Extra
You can also observe this change from multiple views if needed. For example, if you had a TabBar and wanted to switch the active tab and have that tab's stack handle the navigation, that would be done as such:
enum ActiveTab: Hashable {
case post(post: Post)
case profile
}
struct ContentView: View {
@Environment(\.deeplink) var deeplink
@State var activeTab = ActiveTab.home
var body: some View {
TabView {
HomeView()
.tag(ActiveTab.home)
AccountView()
.tag(ActiveTab.account)
}
.onChange(of: deeplink) { link in
// Set the active tab to the correct one, in this case, for deeplinking into a post,
// we'll set the active tab to home.
switch link {
case .post:
activeTab = .home
case .profile:
activeTab = .account
case .none: break
default: break
}
}
}
}
Then in your HomeView for example, you would navigate to the post
struct HomeView: View {
@Environment(\.deeplink) var deeplink
@State private var path: [Deeplink] = []
@State private var posts: [Post] = []
var body: some View {
NavigationStack(path: $path) {
List(posts, id: \.id) { post in
NavigationLink(post.title, value: Deeplink.post(post))
}
.navigationDestination(for: Deeplink.self) { link in
destination(for: link)
}
}
.task {
do {
posts = try await API.loadPosts()
} catch {
posts = []
}
}
.onChange(of: deeplink) { link in
switch link {
case let .post(post):
path.append(.post(post))
case .none: break
default: break
}
}
}
@ViewBuilder
func destination(for deeplink: Deeplink) -> some View {
switch deeplink {
case let .post(post):
PostView(post)
default:
EmptyView()
}
}
}