Scalable Navigation With Deep Links in SwiftUI | by Riccardo Cipolleschi | Jul, 2022

Use deep links to navigate to the right screen in complex apps

Photo by Victor on Unsplash

When we browse the web, sometimes we click on a product, and we see the Amazon app opening exactly on the page of that product. Other times, after answering a friend on Messenger, we take a look at their profile, and the Facebook app opens directly on it.

These are examples of navigation from one app to another using deep links. This technology lets us connect our users with some internal screens of our apps. These screens are deep into the navigation tree, but deep links let us reach them without asking the users to navigate to that screen manually.

Today, I’d like to show you how to set up deep links in your app and how to handle them in a scalable way.

Let’s start by creating a new Xcode project. We can select Swift as the programming language and SwiftUI as the user interface framework. Then, we can follow the prompt until Xcode opens for us with a standard SwiftUI project.

To configure the deep links, we need to:

  1. Select our project in the Project Navigator (⌘+1)
  2. Select our app under in the Target panel
  3. Select the Info tab
  4. Expand the last option, URL Types
  5. Click on the small + button at the bottom of the panel
  6. Fill out the form that has appeared.
1* Lg comGmGNvnRP0Rvnw1g
How to set up a URL for deep links

In the form, there are four fields we can fill plus another section for type properties. The only important parameter to set is the URL Scheme in the top-right. This parameter controls how the URL must be shaped for the app to recognize it.

So, for example, if we set it to be deeplinkapp every time the user clicks on a link on Safari that has the shape deeplinkapp://, iOS will ask the user whether they want to open the URL in our app or not. Whenever another app tries to UIApplication.shared.openURL("deeplinkapp://"), iOS will ask the user whether they want to open the URL in our or not.

An app can respond to multiple URL Schemes. This can be useful to direct users to different tabs or different sub-sections of the app. We can also support different schemes if our app is known by different names. To add more URL Schemes, click on the small + button on the bottom and configure them.

SwiftUI makes many things very easy. Responding to a deep link is one of those things. To respond to a deep link, we need to add the onOpenURL modifier to any View that is loaded when the app launches. The code can look like this:

Basic handling of a deep link

Notice that we can also add multiple onOpenURL modifiers in complex view. When this happens, it is interesting to understand how they are handled. Consider a situation like this:

A VStack with a view and an HStack with two views.

When a deep link is passed to the app, we can observe this output:


There is no evident logic in how the link is propagated: the system asks first to ContentView1 then to ContentView2 (that is contained by a different container). After that, instead of asking the sibling of ContentView2, it queries the external container, then the internal container and, finally, the last view.

We can’t make any assumption on how the deep links will be passed to the rendered views.

When an application grows, it is not feasible to try and handle all the possible navigation paths in a single place. If we try to do that, we are going to violate many software engineering principles, and our code could soon become spaghetti code: our deep link manager has to know all the possible screens we can reach with deep links, access different data sources, and create the data for all the screens.

That’s not good architecture, and it can become complex very quickly. There is a better approach to it: we can use the Chain of Responsibility pattern and split the task among different objects.

Chain of responsibility pattern

The pattern allows us to structure hierarchically multiple objects involved in solving a specific problem. All these objects share the same interface, and they have to carry out a similar task: for example, they have to decide whether they can handle a deep link and how to handle it.

The hierarchy can be queried in two directions: bottoms-up, from the children to the root, or top-down, from the root to the children.

In a bottoms-up approach, the leaf is the first element queried. It checks first whether it can carry out the task. If yes, the task is done. If not, it asks its parent to do the required task. The parents follow the same process: check whether it can do the job and, if not, it asks its parent, and so on until they find something able to execute the task or the system reports the impossibility of executing it.

We can find this pattern in many areas of software engineering. Touches in our views are managed in this way. Exceptions are thrown using this pattern. When we build complex SwiftUI views, we delegate the rendering of subviews to their body property.

How to implement it at scale

We are considering the case of a complex application, split into modules, which has a single CompositionRoot that is the only element that knows the whole app. It’s the CompositionRoot that creates the hierarchy of DeepLinkParsers.

As the first step, let’s define a protocol for the DeepLinkParsers:

The protocol defines that a DeepLinkParser should have two methods: one to check whether it can handle a deep link and another one to actually handle it.

Then, we can implement how to specifically handle part of the deep link in each module without leaking any information from other modules.

For example, the TabDeepLinkParser could be an object that parses the initial part of the deep link and switches between different tabs.

Its implementation can look like this:

The TabDeepLinkParser is initialized with an array of tabs, an array of other DeepLinkPasers and an action to be performed if the deep link can be handled.

Then, we implement the first method of the protocol: canHandleDeepLink. This method returns true if it can handle the link. The TabBar can handle the link when the host of the URL is one of the tabs and if there is at least a child of this parser that can handle the link properly.

Given that we are querying all the children, the check for the possibility to handle the deep link mustn’t require a lot of work or intensive operations. We want to know if we can handle the link as fast as we can.

Finally, we implement the handleDeepLink by executing the action and then asking the children that can handle the deep link to actually handle it.

Note: This is one of the possible implementations. Depending on your app logic you may not want to loop through all the possible handlers. You could look for the first or for the last, for example. You may also implement some sort of priority queue, depending on the structure of the deep link.

Then, we can just repeat this pattern in different modules, handling different parts of the deep link. A ProductList view, for example, could implement a deep link parser that checks for the presence of an id parameter and do something if that parameter is there:

This parser has no children, meaning that it is a leaf parser in our chain. It knows how to handle a deep link with the id paramenter and it knows that it has to perform an action with the id it extracts.

Finally, the CompositionRoot can put the parser together. All the parsers are pieces of pure logic and don’t have any reference to the UI. At the end of the day, we want to use them to navigate to the proper screen. The CompositionRoot bridges the parsers’ logic with the UI by creating the hierarchy of deep link parsers and by feeding them the proper actions.

An example implementation is the following:

The DeepLinkApp works as the CompositionRoot. It declares two @State variables to keep track of the current selection. Then it defines the tabs we want to use in the app.

The body property contains the MainTabView with our tabs, and it uses the onOpenURL modifier to handle the deep link.

When a deep link is received, it creates the proper hierarchy of parsers: the TabDeepLinkParser with a ProductDeepLinkParser child. The action used in the first parser changes the selectedTab property. The action used in the second parser selects the proper product.

In today’s article, we learned what deep links are and how to structure an app to handle them. We also learned how to keep the parsers’ logic separated from each other. This improves how composable the solution is, and it simplifies the logic we have to write in every single parser.

We learned about the Chain of Responsibility pattern and how it can be used to compose the parsers together.

Everything discussed today can be found in this GitHub repository. It doesn’t contain only the code we saw in this article. It also contains other interesting things, like how to implement this same system without using AnyView, if we accept to know in advance which tabs we will need to create, for example.

Deep links are very powerful, and this article just scratched their surface. They can be used to implement many other sophisticated functionalities — from testing to tracking and much more.

News Credit

%d bloggers like this: