Software Architecture
-
September 20, 2023

Unleashing the Power of Composable Architecture: Building Robust and Scalable iOS Applications

The Composable Architecture or TCA is a framework that allows us to build apps in a compositional way. This means we can create independent entities with their own functionalities that can work together as a system. To do so, the library provides us with some core tools that can be used to solve many problems we face on a daily basis when building applications.

Which problems does TCA solve?

SwiftUI can present some challenges regarding state management, as there is no standard architecture for handling business logic and models. TCA’s unidirectional data flow and predictable state management are both inspired by Redux, which has been shown to be a powerful and effective way of managing state in web applications. By building on these concepts and adapting them for iOS development, TCA provides a composable and testable approach to building iOS applications that can help improve code quality and developer productivity.

Using TCA can also make a project more scalable. By applying a composable architecture, each object or feature is responsible for itself, but can still share its states or actions with parents and children, making it ideal for scalability and composability.

Now let's start by looking at this nice diagram that explains, in broad strokes, the functioning of TCA

As you can see, the two big players in this frameworks are UI and DOMAIN

To build a feature using the Composable Architecture you define some types and values that model your domain:

  • State: A type that describes the data your feature needs to perform its logic and render its UI. It has to conform Equatable
  • Action: A type that represents all of the actions that can happen in your feature, such as user actions, notifications, event sources, and more.
  • Reducer: A function that describes how to evolve the current state of the app to the next state given an action. The reducer is also responsible for returning any effects that should be run, such as API requests, which can be done by returning an Effect value.
  • Store: The runtime that actually drives your feature. You send all user actions to the store so that the store can run the reducer and effects, and you can observe state changes in the store so that you can update UI.

Some of the benefits of using TCA are that you will instantly unlock testability on your feature, and you will be able to break large, complex features into smaller domains that can be glued together.

As a basic example, consider a UI that shows a card for a particular store product, this card will show the basic information of the product and there will be a like button for adding this one in favorite.

To implement this feature we create a new type that will house the domain and behavior of the feature by conforming to ReducerProtocol.

Lets see it In practice

CODE: https://gist.github.com/tarruk/fa438b21d40e7988d183d7f6a72155d3.js?file=ProductCardDomain.swift

Let's take a look at how our view behaves when implementing TCA

CODE: https://gist.github.com/tarruk/1e5f86c08a12d5dd3ab5e27d048f7969.js?file=ProductCardView.swift

As we can see, the line let store: StoreOf<ProductCardDomain> is required to declare the Store of the domain from which we will consume the data.

Next we have to add the block  WithViewStore that is a view helper that transforms a Store into a ViewStore so that its state can be observed by a view builder.

CODE: https://gist.github.com/tarruk/d9df7ac43656f39deb0b8b42c1b1d7cf.js?file=ProductCardView.swift

Now we can access all the states and actions that are inside the store. As an example, we can see how the Like button can trigger the .likeButtonTapped action through the viewStore.

But not so fast, right?

Although TCA makes our work easier in many ways by helping us componentize and structure our code, it is intended to have a unidirectional data flow. This makes it different from what we are used to in SwiftUI, and therefore makes it a bit complex to learn or master. One of those cases where we have to relearn is when working with asynchronous functions. In native SwiftUI, we could solve the problem using async functions, Result, completions, or some other mechanism that allows us to establish a bidirectional path. In TCA, being unidirectional forces us to create an action for every new effect that can be generated, for example:

Let's suppose that we need to make a simple signUp call to the backend. In native SwiftUI, we would have to use an asynchronous function to call the service, wait for the response, and then evaluate whether it was successful or not. However, in TCA, we would approach this kind of operation differently.

We define an action type within our domain that includes the signUp action, which makes the call to the API. Additionally, we have another action called receiveAuthResult that will receive the result of the API call and evaluate the response.

CODE: https://gist.github.com/tarruk/b1bac9e694fbe490aa9c3c0b6085e164.js?file=ProductCardDomain.swift

In the Reducer, we use the necessary logic to handle the .signUp action, which then triggers a result or effect (using .task). This effect will wait for the result of the sign-up call and then trigger the .receiveAuthResult action to manage the result

CODE: https://gist.github.com/tarruk/32039d2004ffedb26a5d7a4337cd505d.js?file=ProductCardDomain.swift

We have explored some of the capabilities of TCA. This framework proves to be highly powerful and can serve as a valuable tool in creating more granular components, facilitating state sharing across the entire app through a single source of truth, and simplifying mocking and business logic. I invite you to discover TCA for yourself and form your own questions and opinions about its potential

Life is not always sunshine and rainbows

While The Composable Architecture (TCA) offers many advantages, there are some considerations to keep in mind before adopting it in a project:

  • Since it is an external library, the app will have a strong dependency on it and it would be quite difficult to replace it in case that is desired in the future.
  • It may not be convenient to use it just in certain parts of the project. If the project is already implementing another architecture such as MVVM or MVC, it may be difficult to apply or integrate this library, making it not worth the effort in those cases. To fully benefit from TCA, it is best to use it throughout the entire project.
  • TCA can be more challenging to learn compared to other architecture models like MVVM or MVC. However, it’s important to remember that TCA is not just an architecture model, but a framework with its own set of tools that can greatly facilitate the development process.

Overall, while TCA may not be the best fit for every project, it can offer significant benefits for those willing to invest the time and effort to learn and apply it effectively.

* Ref: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture