Skip to content

Examples

Timur Yusipov edited this page Sep 12, 2017 · 4 revisions

###Best practices for supporting Deeplinks

A generally asked question is how to switch tabs using Marshroute. It is simple. You just cannot do it, because changing tabs is not a transition between modules, that is tracked by Marshroute. Why? Because usually you do not need to save history of such transitions, so we decided to postpone this feature implementation (probably forever).

The requirement to change tabs appeared with the beginning of DeepLinks support. We change tabs programatically and out of Marshroute knowledge.

First of all, we have a root module called Application. It is represented via a root UITabBarController. Its Presenter plays an important role in coordinating tabs state.

First of all ApplicationPresenter decides whether a user can select a tab, because this tab may require authorization. In such cases ApplicationPresenter displays the Authorization module and waits for it callback to programmatically select a requested tab if a user passes authorization.

Secondly, ApplicationPresenter is a starting point for changing application's navigation state in response to DeepLinks. That means all DeepLink handlers ask ApplicationPresenter to show some module via a related protocol.

For example, lets pretend we have a ChannelDeepLinkHandler which gets invoked in response to a com.myapp://channels?channelId=1234 DeepLink. This DeepLink leads to a chat with some another user (we call them channels). So the purpose of that DeepLink handler is to show Channel module. This DeepLink handler is simple, because it does not have to load or fetch any data before proceeding, because it can get all required info right from the DeepLink. There may be more complicated situations, where the DeepLink handler will send some requests to the server to update the data in the cloud. We will consider tho most simple case.

As mentioned earlier, all DeepLink handlers ask ApplicationPresenter to change application's navigation state. However, ApplicationPresenter gets injected to the ChannelDeepLinkHandler not directly, but being hidden behind a protocol: ApplicationModule. You can go even further and compose an ApplicationModule protocol from simplier ones, each one specific for a DeepLink handler (e.g.: ChannelDeepLinkHandlerHelper). This is a prefered way, because all DeepLinkHandler is likely to know about is a function like func showChannel(channelId: String)

Anyway, ApplicationPresenter will implement func showChannel(channelId: String) and that is where the magic may take place.

ApplicationPresenter may decide to switch tab before proceeding. There are some edge cases in programmatic tab changing.

  1. Let's pretend your current tab's module hides the tab bar (usually on push) in contrast to a module at the target tab. If we change the tab to show a Channel module, the user will get frustrated after changing the tab back, because the tab bar will suddenly get hidden.
  2. Your current tab may have one or more modally presented modules. They are definitely likely to be dismissed prior to changing tabs, because UIKit may just not work fine unless you do that.

Moreover, your currently displayed module may already be an instance of Channel module. It is a common practice to dismiss old Channel with respect to a new one. Such module replacement may also fall under a requirement to be non-animated.

So we come up with a tough task. One possible solution is to start tracking modules depending on their relation to DeepLinks and their features of hiding the tab bar.

That is how the showChannel implementation may look like:

func showChannel(channelId: String) {
    router.popToTabBarAtTab(currentTab) // (1)
        
    view?.setSelectedTab(.channels) // (2)
        
    router.dismissDeepLinkDrivenModule(
        name: .channel,
        atTab: .channels
    ) // (3)
        
    router.showChannel(channelId) // (4)
}

Here is the detailed explanation of the steps

  1. Dismiss all modally presented modules. Pop to a closest module that displays the tab bar
  2. Change the tab: i.e. a selected index of your UITabBarController
  3. Dismiss currently displayed instance of Channel module if exists
  4. Finally, show the Channel module

Here the ApplicationPresenter does not know about the Channel module as a type and refers to it via an enumeration (we call it DeepLinkDrivenModuleName). This enumeration contains names of modules that either show tab bar or may be opened via a DeepLink. So ApplicationPresenter takes control over the navigation state, but in a declarative fashion.

The ApplicationRouter also forwards control to an injected service. You can find a simplified example of that service in the demo project (see ModuleRegisteringServiceImpl). In Avito we have a slightly more complicated version of this service to satisfy DeepLink-driven behaviors.

In general, the service stores information about modules. It receives that information from the assemblies of those modules. You can find the demo of such process in the demo project (see AdvertisementAssemblyImpl)

Here are main responsibilities of this module-tracking service:

  1. Store the information about modules: their names, router seeds, transition handlers and tab presentation styles.
  2. Compute the tab for a newly registered module
  3. Compute which modules should be dismissed for a concrete tab (e.g.: to make tab bar visible) and invoke methods from TransitionsHandler protocol to alter the navigation state of the application.
  4. Control module duplicates via computing distance between modules.
  5. Disable animations during popToTabBarAtTab(_:) or dismissDeepLinkDrivenModule(_:) calls (via implementing TransitionsCoordinatorDelegate protocol)

In Avito we have much more complicated DeepLinks, which may:

  1. Interrupt current flow.
  2. Start a new flow of several modules.
  3. Wait for a completion of the whole flow to notify the server.
  4. Return the navigation state to the state before interrupting.

In such cases previous flows (chains of view controllers) can become invalid (because each controller may start displaying outdated information). To handle such situations, the DeepLink handler's implementation may notify some service about a possible server state modification, to force relevant view controllers to reload the data.

Previous flow's state restoration is valuable only in cases where the tab is not changed during DeepLink handling, because modules of those tabs may become deallocated after popping to tab bar, for instance. The module-tracking service takes part in registering flows as well, but the explanation of this process is out of scope of this wiki page. All I can say, that it this feature was not a big deal to implement and the whole architecture did not suffer.

Clone this wiki locally