Navigation and Deep Links in SwiftUI

With many websites now having a mobile app - we are seeing more apps featuring deep links. Deep Links are hyperlinks which when tapped or visited takes users to a specific piece of content in the app. For eg: If you tap on a link to someone's tweet on your iOS device, iOS opens up Twitter app, and takes you to that user's tweet. Deep links can be attached to a specific tab, a particular view or a section of page etc. In this article we will go over how to set up a URL scheme and then we will deep link tabs and specific detail page for content.


There are two ways to setting up deep linking. First and the easiest way is by using a URL scheme and the other way is using Universal links. Lets set up URL Scheme:


Its vital to understand how the url schemes look like: someappname://id. The someappname is the URL you add in Xcode. Then you have a colon : with two forward slashes //- after that you have the id tag. This id tag is what we will decode and use for navigation. Head over to Xcode and do the following:

  1. Click on the Project name
  2. Click Info
  3. Expand URL Types
  4. Add the name of your app or link in URL schemes text field



I have named my app pariscafe, so whenever we type pariscafe://search - it will take us to the search tab. And so on!

Data Model

Lets talk about the data model and an important property that you need to add in all the models that you want to support deep link. Here is the data model I will be using for this project:

struct ParisCafe : Identifiable{
    var id: String
    var name: String
    var rating: String
    var picture: String
}

class DataSource {
    static let data : [ParisCafe] = [ParisCafe(id: "PRS100", name: "Café de Flore", rating: "4/5", picture: "flora"),
                                    ParisCafe(id: "PRS101", name: "The Caféothèque of Paris", rating: "4.2/5", picture: "TheCaféothèqueofParis"),
                                    ParisCafe(id: "PRS102", name: "Cuppa - Salon de Cafe", rating: "4.3/5", picture: "cuppa"),
                                    ParisCafe(id: "PRS103", name: "Republique of Coffee", rating: "4.1/5", picture: "rofcoffee"),
                                    ParisCafe(id: "PRS104", name: "fringe", rating: "4.6/5", picture: "fringe"),
                                    ParisCafe(id: "PRS105", name: "Malongo Cafe", rating: "4.4/5", picture: "TheCaféothèqueofParis")]
}

As you can see the ParisCafe struct conforms to Identifiable protocol which makes it mandatory for us to add the id property. The id property is of type string. We will be implementing deep link in such a way that if the user writes the following hyperlink in Safari pariscafe://PRS103 then it should direct the user to our app and open the detail page for that particular cafe. So, id is extremely important for deep linking.

The Basic App Structure

Our app will consist of a tab view containing 3 tabs (Home, Search and Setting). Each of these tabs will be deep linked. In the Search view we will have a list of cafes and on tapping one should take you to a detail page for that cafe.


Before moving to the views lets look at enum ParisCafeTab which basically houses all the tab names.

enum ParisCafeTab: String {
    case home =  "home"
    case search =  "search"
    case setting = "setting"
}


Then we also have an ObservableObject called AppViewModel:

class AppViewModel: ObservableObject {
    @Published var currentTab: ParisCafeTab = .home
    @Published var currentDetailCafeID: String?

    func checkDeepLink(url: URL)->Bool {
        // more on this function later
    }

    func checkInternalDeepLinks(host: String)-> Bool {
      // more on this function later      
    }
}


The ObservableObject just contains two Published properties - currentTab and currentDetailCafeID. Both these properties are used to track the current tab and the current detail page to be shown. We will be decoding the deep link and publishing changes to these properties which will update the view and take us to that specific piece of content. Since this is an ObservableObject we will be injecting it as an EnvironmentObject to the ContentView, so that we can access this anywhere in the app.

App.swift file

Lets start with ParisProjectApp swift file, here we are creating the AppViewModel() instance and injecting it in the ContentView and all its children views using .environmentObject function. Also here, we can see another modifier called .onOpenURL - this function is called every time we open the app using the deep link (with that convention as mentioned above). We pass that url to checkDeepLink function which we are yet to implement inside the ViewModel.

@main
struct ParisProjectApp: App {
    @StateObject var appVM : AppViewModel = AppViewModel()
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(appVM)
                .onOpenURL { url in
                    if appVM.checkDeepLink(url: url) {
                        print("got here from Deep link")
                    }else {
                        print("no deeplink found")
                    }
                }
        }
    }
}


In order to access the environment object in children views all we need to do is add the following line @EnvironmentObject var appVM : AppViewModel.

ContentView

struct ContentView: View {
    @EnvironmentObject var appVM : AppViewModel
    var body: some View {
      MainTabViews()
    }
}

MainTabViews

struct MainTabViews: View {
    @EnvironmentObject var appVM : AppViewModel
    var body: some View {
        TabView(selection: $appVM.currentTab) {
            Text("Home")
                .tag(ParisCafeTab.home)
                .tabItem{
                    Image(systemName: "house.fill")
                }
            SearchView()
                .tag(ParisCafeTab.search)
                .tabItem{
                    Image(systemName: "magnifyingglass")
                }
            Text("Setting")
                .tag(ParisCafeTab.setting)
                .tabItem{
                    Image(systemName: "gearshape.fill")
                }
        }
    }
}


In this view, we are binding the TabView selection property to AppViewModel's published property of .currentTab. Each Tab has a tag which is gets from the ParisCafeTab enum. So if we need to select search tab, all we need to do is to assign appVM.currentTab to say ParisCafeTab.search and search tab will be selected.

SearchView

struct SearchView: View {
    @EnvironmentObject var appVM : AppViewModel
    @State var data = DataSource.data
    var body: some View {
        NavigationView {
            List {
                ForEach(data) { cafe in
                    NavigationLink(tag: cafe.id, selection: $appVM.currentDetailCafeID) {
                        DetailView(cafe: cafe)
                    } label: {
                        HStack {
                            Image(cafe.picture)
                                .resizable()
                                .aspectRatio(contentMode: .fit)
                                .frame(width: 150, height: 150)
                            Text(cafe.name)
                        }
                    }
                }
            }.navigationTitle("Search")
        }
    }
}

@ViewBuilder
func DetailView(cafe: ParisCafe)-> some View {
    VStack {
        Image(cafe.picture)
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: 350, height: 350)
        Text("⭐️ Rating: ")
        Text(cafe.rating)
    }.navigationTitle(cafe.name)
}


In SearchView, we have two properties the appVM which is environment object and the data (array of type ParisCafe). Then wrapped by a NavigationView, we have a List iterating through the data array. Then we are grabbing the data of type ParisCafe, and creating a NavigationLink. NavigationLink will allow us to push to the detail view when the item is tapped. The label for the NavigationLink is just a HStack with an image of the cafe and the cafe name.


On tapping the list item, we will move to the DetailView. The DetailView is built using a ViewBuilder. Any function assigned with the keyword @ViewBuilder, allows you to return some kind of a view. So, inside this DetailView method, we are creating a simple view comprising of a VStack containing the image and rating of the cafe.


Do note that the NavigationLink is taking two parameters, id and selection. The id is the cafe item's id and the selection is binded to appVM.currentDetailCafeID. Both of these will be used with deep linking.


So now that all the views have been discussed, now lets get back to the AppViewModel and implement checkDeepLink and checkInternalDeepLinks methods.


Here is the complete AppViewModel class:

class AppViewModel: ObservableObject {
    @Published var currentTab: ParisCafeTab = .home
    @Published var currentDetailCafeID: String?

    func checkDeepLink(url: URL)->Bool {
        guard let deepLinkComponent = URLComponents(url: url, resolvingAgainstBaseURL: true)?.host   else {
            return false
        }
        print(deepLinkComponent)
        if deepLinkComponent == ParisCafeTab.home.rawValue {
            currentTab = .home
        }else if deepLinkComponent == ParisCafeTab.search.rawValue {
            currentTab = .search
        }else if deepLinkComponent == ParisCafeTab.setting.rawValue {
            currentTab = .setting
        }else {
            return self.checkInternalDeepLinks(component: deepLinkComponent)
        }
        return true
    }

    func checkInternalDeepLinks(component: String)-> Bool {
        if let index = DataSource.data.firstIndex(where: { cafe in
            return cafe.id == component
        }){
            currentTab  = .search
            currentDetailCafeID = DataSource.data[index].id
            return true
        }

        return false

    }
}


Lets first discuss checkDeepLink function. The checkDeepLink takes in the URL and we will get the components using URLComponents method. This URLComponents method should return us with the string after someappname://. So, essentially if you have the following URL scheme, someappname://search, deepLinkComponent should be assigned the value of search. We will use this string value and check which tab or NavigationLink item we should call.



Next, we will check if the deepLinkComponent is equal to any of the tabs tag name using the ParisCafeTab enum. If it is equal to any of the tab tag name, we will assign that enum case to the published property currentTab. So, if the app is opened using the following deep link pariscafe://home, it will invoke this function, and decode the string, if the string is equal to any of the enum cases, we will assign the currentTab published property to its respective enum case, causing the view to refresh and showing us the Home view.


Now, lets move on to more deeper internal links such as opening detail view using the cafe's id. If the decoded deepLinkComponent string is not equal to any of the tabs tag name, then we will call the checkInternalDeepLinks method. In this method, we will grab the index of the item in the data source whose id is equal to the component that is passed in as function parameter. Then we will first switch to the search tab since that is where we have the list of the cafes. We will then change the second Published property inside our view model which is currentDetailCafeID to the id of the cafe using the index we grabbed earlier.


This will basically refresh the search view and since the NavigationLink's selection argument has changed it will push to that item's detail view.


So if we write the following: pariscafe://PRS104 - it will open the app, invoke the checkDeepLink method, since PRS104 isn't equal to any ParisCafeTab enum cases, it will invoke checkInternalDeepLink and pass PRS104 string. We will go through the data array and look for the index of the item with id of PRS104. That index will then be used to change the selection binding property (currentDetailCafeID), and we will have the detail page showing.


The full source code of this project is available on GitHub