-
Notifications
You must be signed in to change notification settings - Fork 30
Examples
###Best practices for supporting Deeplink
s
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 DeepLink
s 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 DeepLink
s. 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 channel
s). 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.
- 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. - 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 DeepLink
s 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
- Dismiss all modally presented modules. Pop to a closest module that displays the tab bar
- Change the tab: i.e. a selected index of your
UITabBarController
- Dismiss currently displayed instance of
Channel
module if exists - 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:
- Store the information about modules: their names, router seeds, transition handlers and tab presentation styles.
- Compute the tab for a newly registered module
- 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. - Control module duplicates via computing distance between modules.
- Disable animations during
popToTabBarAtTab(_:)
ordismissDeepLinkDrivenModule(_:)
calls (via implementingTransitionsCoordinatorDelegate
protocol)
In Avito we have much more complicated DeepLink
s, which may:
- Interrupt current flow.
- Start a new flow of several modules.
- Wait for a completion of the whole flow to notify the server.
- 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.