E2EHIRING Logo
Jobs
Jobs
courses
Courses
mentorship
Mentorship
more
Moredropdown
E2EHIRING Logo
more
Jobs
Jobs
courses
Courses
mentorship
Mentorship
HomeSepratorIconBlogsSepratorIconSwiftUI - Navigation Architecture using Composable Architecture (TCA)SepratorIcon

SwiftUI - Navigation Architecture using Composable Architecture (TCA)

Han SoloJasim Ahmed
calendar23 Jun 2022
poster

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,

struct View0State: Equatable {
 var view1State: View1State? = nil
}
struct View1State: Equatable {
 var view2State: View2State? = nil
}

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

struct View1State: Equatable{
  var view2State : View2State? = nil
}

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

enum View1Action {
  case presentView2
  case didDismissView2
  case view2(View2Action)
}

Example 1

Let’s add the actions in the reducer,

let View1Reducer: Reducer<View1State, View1Action, View1Environment> = Reducer.combine(
    
    .init { state, action, environment in
        struct View1CancelId: Hashable {}
        switch action {
        case .presentView2:
          state.view2State = View2State()
          return .none
        case .didDismissView2:
          state.view2State = nil
          return .none
        case .view2(_):
          return .none
        }
      }
    )

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.

.navigate(
     using: store.scope(
        state: \.view2State,
        action: View1Action.view2
        ),
      destination: navigateToView2,
      onDismiss: {
        ViewStore(store.stateless).send(.didDismissView2)
        }
    )

Let’s break down the above code,

store.scope(
  state: \.view2State,
  action: View1Action.view2
  )

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.

 func navigateToView2(for store:Store<View2State, View2Action>) -> some View {
        // create and configure destination view here
   View2(store: store).navigationBarTitle("").navigationBarHidden(true)
}

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

 var view2Store: Store<View2State, View2Action> {
       return store.scope(
           state: { $0.view2State ?? View2State() },
           action: view1Action.view2
       )
   }

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,

View2Reducer.pullback(
    state: \View1State.prosState,
    action: /View1Action.pros,
    environment: { environment in
        View2Environment(
         view2Client: environment.view2Client,
         mainQueue: environment.mainQueue,
         uuid: environment.uuid
      )
    }
  )

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,

View2Reducer
  .optional()
  .pullback(
    state: \View1State.view2State,
    action: /View1Action.view2,
    environment: { environment in
        View2Environment(
         view2Client: environment.view2Client,
         mainQueue: environment.mainQueue,
         uuid: environment.uuid
      )
    }
  )

.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.

.presents(
        View2Reducer,
        state: \.view2State,
        action: /View1Action.view2,
        environment: { environment in
            View2Environment(
                view2Client: environment.view2Client,
                mainQueue: environment.mainQueue,
                uuid: environment.uuid
            )
        }
    )

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

let View1Reducer: Reducer<View1State, View1Action, View1Environment> = Reducer.combine(
    
    .init { state, action, environment in
        struct View1CancelId: Hashable {}
        switch action {
        case .presentView2:
          state.view2State = View2State()
          return .none
        case .didDismissView2:
          state.view2State = nil
          return .none
        case .view2(_):
          return .none
        }
      }
    ).presents(
        View2Reducer,
        state: \.view2State,
        action: /View1Action.view2,
        environment: { environment in
            View2Environment(
                view2Client: environment.view2Client,
                mainQueue: environment.mainQueue,
                uuid: environment.uuid
            )
        }
    )

Example 5

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

Button(action: {viewStore.send(.presentView2)}){
    Text("Sample Text")
}

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,

enum View2Action{
  case navigateBackToView1
}
let View2Reducer: Reducer<View2State, View2Action, View2Environment> = Reducer.combine(
    
    .init { state, action, environment in
        struct View2CancelId: Hashable {}
        switch action {
        case .navigateBackToView1:
          return .none
        }
      }
    )

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):

let View1Reducer: Reducer<View1State, View1Action, View1Environment> = Reducer.combine(
    
    .init { state, action, environment in
        struct View1CancelId: Hashable {}
        switch action {
        ...
        case .view2(navigateBackToView1):
          return .init(value: .didDismissView2)
        case .view2(_):
          return .none
        }
      }
    )

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!

Recent Posts

Rapid Changes in the Coding World: Need for High Skilled Programmers e2eHiring Bridges this Gap with the Mentorship Program April 2023

Rapid Changes in the Coding World: Need for High Skilled Programmers e2eHiring Bridges this Gap with the Mentorship Program April 2023

How to publish your Android app on Google Play Store

How to publish your Android app on Google Play Store

Creating Dynamic User Interfaces with Android Motion Layout

Creating Dynamic User Interfaces with Android Motion Layout

Bean Life Cycle

Bean Life Cycle

Pom.XML

Pom.XML

copycopycopycopy

Han Solo

Recent Posts

Rapid Changes in the Coding World: Need for High Skilled Programmers e2eHiring Bridges this Gap with the Mentorship Program April 2023

Rapid Changes in the Coding World: Need for High Skilled Programmers e2eHiring Bridges this Gap with the Mentorship Program April 2023

How to publish your Android app on Google Play Store

How to publish your Android app on Google Play Store

Creating Dynamic User Interfaces with Android Motion Layout

Creating Dynamic User Interfaces with Android Motion Layout

Bean Life Cycle

Bean Life Cycle

Pom.XML

Pom.XML