Routing (Navigation) with ReSwift in iOS apps

Sethu
6 min readFeb 18, 2018

I have been working with ReSwift to build iOS apps. ReSwift is a port of redux to iOS. The idea is to have your complete view state inside a global store. Your view then becomes a function of your state. Given a state, you know how your view is going to look. From your view you dispatch an action which is the only way you can change your state. Your view will then re-render itself from the information on the state.

In this post I wanted to explore how we used ReSwift to build navigation flows in our app. If you are looking to get started with ReSwift, please have a read through the Getting Started guide on ReSwift and also this excellent article.

When we were looking at how to approach routing within the app and driving routing through ReSwift, we wanted to build stuff based on these principles:

We should be able to dispatch an action from anywhere in the app to move from one screen to any other screen.

We wanted to use Segues on the storyboard, to manage our transitions.

We also didn’t want to limit ourselves to only use Segues for transitions, if there was a need for programmatic transition, it should be possible.

All routing should be handled from one central place to make it easier to debug and maintain

At any point in time, if we look at the state, it should reflect the exact screen we are in currently.

How we coded the solution using the above principles in mind, is best explained with an example:

This is app we are implementing:

On the storyboard this is what it looks like:

The above image sort of summarises the solution.

You can transition from a screen to another screen using 2 flows.

  1. Transition — This is the simplest flow. This is essentially either pushing one more screen onto a navigation controller or presenting a screen modally. In the diagram this is moving from the FirstTabViewController to the Top or BottomViewController
  2. Resetting the screen hierarchy — If you are on a screen that is heavily nested deep inside a screen hierarchy and you want to come out of the screen hierarchy and move to a completely new screen hierarchy. In the diagram this is moving from the BottomViewController to the SecondTabViewController. This is mostly required when you are deep linking into the app. You might be anywhere in the app and when you deep link into the app, you will have to reset your screen hierarchy and move to the new screen as required by the deep link. I am providing the above example to illustrate how to achieve resetting the screen hierarchy. You ideally shouldn’t have user flows like the example, as it kills the user experience.

All routing is handled by the AppRouter. There is a single instance of the AppRouter that is instantiated by the AppDelegate that subscribes to the NavigationState. Next, every view controller from which screen transitions happen, has an attached Router.

When we need to move from one screen to another screen, such as when the Top button is clicked, we fire an action like this one:

NavigationAction looks like the above..

Here Screen is an enum with all possible screens on your app and data is a dictionary that you can use to pass on information that the new screen needs to render itself.

When the transition action is fired, the NavigationState is changed and the new state has a flag called ‘transition’ set to true on it. Further, the transition data is set to the dictionary we are passing in.

NavigationState

Once the navigation state is changed, what happens next? The below image illustrates how the transition is triggered, carried forward and culminates in the new screen being shown.

The AppRouter listens to any changes to the NavigationState, it then checks if the transition flag is set, if it is, it get the current screen’s router. In this case the current screen showing is the FirstTabViewController, the corresponding router to that view controller is the FirstTabRouter. In the viewWillAppear of the FirstTabViewController, we create a new instance of the FirstTabRouter and add to a RouterRegistry which is a dictionary of type [Screen:Router] .

The AppRouter, gets the current screen from the NavigationState and then queries the RouterRegistry for the current screen’s router. It then calls, show() on that router.

All that the router now does is executes the segue for that screen transition.

Some code that explains all this:

FirstViewController
BaseViewController
FirstViewRouter

The executeSegueBlock needs a little explanation. When we ask the view controller to perform a segue, the view controller calls a prepareForSegue() that allows you to configure your destination view controller before it is rendered. What we are doing in the FirstViewRouter is passing a closure block to the executeSequeBlock() call. The closure is executed in the prepareForSegue(). This allows you to keep the 2 code blocks, calling the segue and preparing the new screen close together making it easier to understand and maintain.

One other point to explain is resetting the screen hierarchy. Sometimes, when you deep link into the app, or if you want a transition from one screen which is deep inside the hierarchy of one screen onto a whole new screen hierarchy, then how do you do it? For example, if you want to move from BottomButtonViewController to the SecondTabViewController.

There are 3 steps involved:

  1. We need dismiss all the screens and reset to the root view controller. In this case back to the TabBarViewController with the first tab showing.
  2. We then need to switch to the second tab.
  3. From that tab we then need to show any other screen we want to present.

In order to do all this, we need to dispatch another action:

What this essentially means is, you don’t care what the current screen hierarchy is, you want the app to reset to the screen hierarchy provided, and the data that you set, is what the screen needs in order to render itself.

In order to achieve this, the AppRouter, first needs to find the base router that is responsible for the current screen hierarchy. In this example, since we are on the BottomButtonViewController screen, the tab that is showing this screen is the FirstTabViewController. This view controller is responsible for the current screen hierarchy. The router associated with this ViewController is the FirstTabRouter. The AppRouter calls dismissScreens() on the FirstTabRouter. This will essentially call popToRootViewController() on the navigation controller. This will complete the first step.

The second step, will then be showing the second tab. We stop there in this example. But if we had to show another screen on top of the second tab, all we had to was execute the segue on the SecondTabViewController or programmatically show the screen that we need to show.

The AppRouter looks like this:

Hope this was useful. Please leave your comments and questions below.

--

--

Sethu

I work as a software developer. I love pecking away on a keyboard coding.