Jobs
Blogs
Courses
Mentorship
Partners
For Employers
e2e logo

SwiftUI - Navigation Architecture using Composable Architecture (TCA)

mac

In this page, we will be looking at how we have architect the navigation using TCA.

Pre-requisite:

  • Basics on The Composable Architecture (TCA), refer to TCA Architecture

This page is a continuation of the above article

As discussed in the TCA Architecture, the Main Store which is associated with the Root View contains children’s Stores.

Figure 1

Let’s take an example,

Since the states of the views are nested with one another, it is important to note that every state is nullable. The navigation in this architecture works when we initialize the state and deinitialize the state.

Initialize the state → navigate to the corresponding view.

Deinitialize the state → navigate back to the root view.



Figure 2

In figure 2, to navigate from View 1 to View 2, we initialize view2State. Similarly, to navigate back to View 1, we deinitialize view2State.

Let’s see how we can achieve this in code,

we have to create 3 actions,

  • Action to initialize the state.
  • Action to deinitialize the state.
  • Action to branch to the next view.

Let’s consider the following state

Let’s try creating the action for initialize and deinitialize.

Example 1

Let’s add the actions in the reducer,

Example 2

As we can see, we have handled the enum for presentView2 and didDismissView2. We will soon cover the need for having .view2(_).

It’s time to write some code in the view.

Usually, we would go for NavigationLink, we have created our extension to navigate using the states of the view.

Let’s break down the above code,

Example 3

The above code uses a scope to get the Store of the child view from the parent view. Here, we get the Store of View2 from View1 using the scope. So, we can see that the purpose for creating case view2(View2Action) in Example 1 is to navigate from view1 to view2, we can also utilize this feature to a greater extent like passing data between views which we will look at in the upcoming pages.

destination: navigateToView2 expects a view with the corresponding Store it.

Notice in the above, we have used store which scop locally, there is a chance that we may run into null exception because view2State is nullable. To avoid crashes, we get the child store from the parent store

To get the child store, we can either go for Example 3 or

and we can use this variable in View2(store: view2Store).

We have successfully initialized the view2State and the action view1Action.view2. But still, the architecture needs to know which reducer to look for in View2 i.e., view2Reducer and the environment.

The process where getting the child reducer from the parent reducer is known as PullBack.

PullBack can be achieved by,

From the above code, we can get the reducer along with the corresponding action and environment.


NOTE: pullback by default, expects a Non-nullable State.


But, if we look back at example 1, we declare State as nullable var view2State: View2State? = nil

We have to add .optional() to the above code,

.optional() enables us to use nullable state.

But when it comes to navigation, there is another way to get a child reducer from the parent reducer.

Example 4


NOTE: The above code expects only Nullable State.


In the above example, if we consider that we are in View1 and want to navigate to View2, then the above code should be written in the reducer of View1. Shown below

Example 5

Let’s consider on a button click, we have to navigate from View1 to View2

viewStore.send(.presentView2) this code initializes the state of the View2 which triggers the navigation.

What if we want to navigate back to View1?

We have to deinitialize the state of View2 staying in View2. But if we notice closely, the action to deinitialize the state of View2 is present in View1Reducer (Refer case .didDismissView2: in Example 5).

If we can access the parent view’s action from the child view’s action, we can able to access .didDismissView2 from View2Reducer.

Let’s consider we have an action and a reducer for View2 as shown below,

Now, we are going to access .didDismissView2 using .navigateBackToView1 which is in View2.

Remember, in example 2, we have a case .view2(_):.

Now, it’s time to dive deep into why we have case .view2(_): in View1Reducer. As we discussed earlier, it is used to connect between the views, i.e., in this example, the reducer of View1 is linked with the reducer of View2 with the help of case .view2(_):, and notice that there is a value associated to .view2 and it is unused which is (_) that returns View2Action, the unused value gives us all actions performed in View2. i.e., we are getting all actions performed on View2 in View1.

Instead of leaving this functionality unused, we can use this value and access the required action in View2.

We can add a case to access the action of View2 as shown case .view2(navigateBackToView1):


NOTE: Make sure that you write the default case case .view2(_): at the end of the switch case.


Whenever we are trying to call navigateBackToView1 action from View2,  case .view2(navigateBackToView1): this case will also get triggered and in the above example we have added return .init(value: .didDismissView2) which means we are redirecting from .view2(navigateBackToView1) action to .didDismissView2 action.

Similarly, suppose we have View1….View10, we can access any action of View10 from View1, which means, we can navigate from View10 to View1.

case .view2(view3(...view10(.someAction))):

We can further use this functionality to send data between the views. We can look at it in upcoming pages.

That's all folks!