Skip to content

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.

DeeplinkManager.swift
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.

App.swift
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()
    }
  }
}