Hello everyone. Lately, I’ve been experimenting with deep links and SwiftUI.

In the realm of iOS development, deep linking emerges as a powerful and indispensable tool, offering developers the means to seamlessly connect users to specific content or features within their applications.

At its core, deep linking allows for the precise navigation to a particular section of an app, rather than merely launching the app’s home screen.

Understanding the fundamentals of iOS deep linking is pivotal for enhancing user experience, promoting user engagement, and, ultimately, unlocking the full potential of your mobile applications.

In today’s tutorial, we’re going to create a simple iOS app with several screens and configure the deeplinking mechanism to access and send data to those parts of our app.

So, grab your coding gear, fire up Xcode, and let’s create a new iOS project!

Deeplinking App Example#

The first step is related to some configuration that will allow deep links to work in our app. To do this configuration, follow the steps below:

  1. Open your Xcode project file.
  2. Select your target.
  3. Select the Info tab.
  4. Expand URL Types section.
  5. Press the + button to add a new URL Type
  6. Fill in the Identifier and Schema

xcode-deeplink-configuration

The Identifier is used to uniquely identify your app in the system, so it’s a good idea to use your reverse domain and the name of your app. The URL Schema is the url used to access your app from other parts of the system, normally we use it as yourschema://action?param1=val1&param2=val2.

Creating the Data Model#

Let’s continue with the creation of an enum that will handle the shape of our data. In this app we have 3 Views that are coordinated with a TabView, so we could define 3 possible states for each one and use the power of enums to define the data state for each view.

Let’s create a file called DeepLink.swift and inside add the following code:

import Foundation

// define the deep link options and associated tag value for each view in the tab view
enum DeepLink {
    case first
    case second
    case third
    
    var tag: Int {
        switch self {
        case .first: 1
        case .second: 2
        case .third: 3
        }
    }
    
    var action: String {
        switch self {
        case .first: "first"
        case .second: "second"
        case .third: "third"
        }
    }
    
    var icon: String {
        switch self {
        case .first: "1.lane"
        case .second: "2.lane"
        case .third: "3.lane"
        }
    }
    
    var title: String {
        self.action.capitalized
    }
}

extension DeepLink {
    static let scheme = "deeplink"
}

The enum contains the three cases we discussed earlier and returns the specific data needed for each view in each computed property. Finally, we’re adding a static property to store the scheme we will be accepting in our app. By doing this we can avoid problems when typing the different strings, and it also serve us as a single source of truth for our data.

Creating the Views#

Let’s continue working on the Views we are going to access via deep links. These are simple views that will display a text, the last one will accept data from outside.

Create the files FirstView.swift, SecondView.swift, ThirdView.swift, then add the following code to each file respectively:

// Add this to FirstView.swift
import SwiftUI

struct FirstView: View {
    var body: some View {
        ZStack {
            Color.green
                .ignoresSafeArea()
            Text("First")
                .font(.largeTitle)
                .fontWeight(.bold)
        }
    }
}

#Preview {
    FirstView()
}
// Add this to SecondView.swift
import SwiftUI

struct SecondView: View {
    var body: some View {
        ZStack {
            Color.yellow
                .ignoresSafeArea()
            Text("Second")
                .font(.largeTitle)
                .fontWeight(.bold)
        }
    }
}

#Preview {
    SecondView()
}
// Add this to ThirdView.swift
import SwiftUI

struct ThirdView: View {
    let message: String?
    
    var body: some View {
        ZStack {
            Color.orange
                .ignoresSafeArea()
            VStack {
                Text("Third")
                    .font(.largeTitle)
                    .fontWeight(.bold)
                Text(message ?? "No message")
                    .font(.headline)
            }
        }
    }
}

#Preview {
    ThirdView(message: "Hello World")
}

Now let’s add the TabView that will handle the navigation between the three views. We are going to reuse the ContentView.swift file to do this. Open it and replace the code inside with this:

/*
 To test the deep links you can use Safari and enter the url with the schema and action defined here
 
 example:
 
 [Schema]   [Action]  [query variables]
 deeplink://first
 deeplink://second
 deeplink://third
 deeplink://third?msg=Hello%20from%20Safari
 */

import SwiftUI

struct ContentView: View {
    // our deep link will change this variable
    @State private var selectedTab: Int = DeepLink.first.tag
    // Stores the message sent from the deep link
    @State private var message: String? = nil
    
    var body: some View {
        TabView(selection: $selectedTab) {
            FirstView()
                .tabItem { Label(DeepLink.first.title, systemImage: DeepLink.first.icon) }
                .tag(DeepLink.first.tag) // we use the tag to navigate in the TabView
            SecondView()
                .tabItem { Label(DeepLink.second.title, systemImage: DeepLink.second.icon) }
                .tag(DeepLink.second.tag)
            ThirdView(message: message)
                .tabItem { Label(DeepLink.third.title, systemImage: DeepLink.third.icon) }
                .tag(DeepLink.third.tag)
        }
        .tint(.black)
        .onAppear { // Styling the TabBar
            let appearance = UITabBarAppearance()
            appearance.backgroundEffect = UIBlurEffect(style: .systemUltraThinMaterial)
            appearance.backgroundColor = UIColor(.white.opacity(0.6))
            UITabBar.appearance().standardAppearance = appearance
            UITabBar.appearance().scrollEdgeAppearance = appearance
        }
        .onOpenURL { url in // handling the deep link
            print("URL: \(url)")
            handleIncomingURL(url)
        }
    }
}

private extension ContentView {
    /// Handles the incoming URL and performs validations before acknowledging.
    private func handleIncomingURL(_ url: URL) {
        // Check that the scheme is the one for our app.
        guard url.scheme == DeepLink.scheme else {
            return
        }
        
        // Checking the URL contains valid components and saving them in a new variable.
        guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
            print("Invalid URL")
            return
        }
        
        // From the components we are getting the URL host, in our case this equals the actions for our deep links.
        guard let action = components.host else {
            print("No action found")
            return
        }
        
        // Setting the data for the given action
        switch action {
        case DeepLink.first.action:
            selectedTab = DeepLink.first.tag
        case DeepLink.second.action:
            selectedTab = DeepLink.second.tag
        case DeepLink.third.action:
            selectedTab = DeepLink.third.tag
            // For the 3rd View we can accept data, we are getting all the data from the query's first element,
            // we could check for specific variable if needed.
            if let messageValue = components.queryItems?.first?.value {
                message = messageValue
            }
        default:
            print("Invalid action")
        }
    }
}

#Preview {
    ContentView()
}

The focus point of the above code is in the modifier .onOpenURL, we are using it to handle the upcoming URL from the deeplink. To handle this url we are defining a function that extracts the information and sets the appropriate data state for our views.

We may want to create a ViewModel for handling the data update and retrieval and make our Views reusable.

Testing the App#

To test the deep links we could use different approaches, from using the terminal to third party apps. To keep this simple let’s use Safari inside the iOS simulator.

Open Safari and in the search bar enter the deeplink URL, we have three valid states:

  1. deeplink://first
  2. deeplink://second
  3. deeplink://third?msg=Hello%20from%20Safari, here the msg query is optional, and you could send any data here.

Here is the app running:

app-deeplink

Conclusion#

And that’s a wrap on our introduction into iOS deep linking with SwiftUI! We’ve covered the ins and outs, from setting up the project to creating views and handling deep links seamlessly. Now armed with this knowledge, we’re ready to level up our iOS app game. So, dive in, experiment, and enjoy implementing deep linking for a more engaging user experience! Happy coding!