The Composable Architecture with an Example Project

Ümit Bozkurt
adessoTurkey
10 min readNov 3, 2023

--

With the introduction of SwiftUI, Apple’s modern framework for building user interfaces, how we design and configure our iOS apps has undergone significant evolution. SwiftUI employs the declarative programming paradigm, which has compelled us to alter our approach to creating UI elements, managing the lifecycle of views, and various other aspects. Incorporating these changes into architectures like MVVM, which we were accustomed to, often proved to be limiting and necessitated straying from the established architectural principles. As developers, we found ourselves stretching and adapting our existing architectures or seeking new architectural approaches offering better compatibility. One such solution is The Composable Architecture(TCA), a Redux-based architectural approach developed by the Point-Free team.

What does TCA promise us?

TCA is a library designed to create applications consistently and comprehensibly. It takes into consideration aspects such as composition, testing, and ergonomics, addressing fundamental issues encountered when developing applications:

  • State Management: Efficiently manages your app’s state, allowing seamless sharing between screens.
  • Composition: Breaks down complex features into modular components that can be easily reassembled.
  • Side Effects: Provides a testable and understandable method for external interactions.
  • Testing: Supports testing of individual and integrated features, ensuring expected business logic.
  • Ergonomics: Simplifies all the above with a user-friendly API, minimizing complexity.

Cons:

Before we dive into the details of the TCA, we should be aware of the negative aspects that come along with what it promises. Especially before starting a large project with this architecture, it is necessary to consider the problems that may arise.

  • It is not a widely used architecture, so developers who will be involved in the project later will have to learn the architecture from scratch.
  • The learning cost is high; even if it is easy to understand the basic flows, it is necessary to know the details of the library well in order to create a complex application.
  • It does not use the default components of SwiftUI, such as @State, @Binding, and @Publisher. Instead, you need to use the structures defined in the TCA library. This increases the learning cost.

Exploring TCA:

We must understand this information before introducing the essential components: TCA is a unidirectional architecture, meaning data flows in one direction.

  • State: State is a struct that defines the data needed for the logic and UI.
  • Action: Action is an enum that encompasses various actions triggered from many sources, such as user clicks, timer calls, and API request results.
  • Reducer: Reducer is a function that determines how actions will be processed and updates the state based on those actions. It can also trigger effects.
  • Effects: An event that modifies the state outside of its local context
  • Store (ViewStore): The store manages all of the above. It receives an action and runs the reducer to modify the state.
Flow — source: https://qiita.com/zeero/items/b77cb689d9a707d94ac7

BookList App

I created a sample project to solidify the architecture and illustrate the connections with examples. The BookList App will consist of a start page that displays a list of books retrieved from this endpoint, and API documentation. Clicking on one of these books will open a new page listing the characters in that book. Let’s get started 🖖.

Create Project

I created an Xcode project named BookList and selected SwiftUI as the interface. Using Swift Package Manager, I included the Composable Architecture library in the project. You can check the readme file for different addition options and details.

As a first step, I’m creating the BookListView, where we will list the books. I’ve added a navigation bar and a button that will initiate the book list download when clicked.

BookListReducer

I’m creating the BookListReducer on a new page. I’ve created a class conforming to the Reducer protocol defined in the library. The State defined within it will hold our data. Action will be an enum that contains actions triggered by the user or the results of our API requests. Based on the triggered actions, the reduce function makes API requests and updates the state.

There is currently no connection between the BookListView and BookListReducer. Before establishing this connection, I’m adding an action (fetchBooks) to set up the structure to fetch the book list from the API. The Reducer function has a return type of Effect. Effects are structures we use to perform actions outside of the local context. We will make the API request using these effects. Since our response models are not ready, we are returning “.none.”

We will pull the book list from the “https://anapioficeandfire.com/api/books" endpoint. I created a “Book” model to parse the response we will receive.

After creating the Book model, I defined a Book array called “books” in the state to store the list of books I will retrieve from the API request and display in the view. Subsequently, I defined an action named “booksFetched” that will be triggered when the API request is completed. This action will update the state with the fetched list of books. I can now send the API request. To do this, I’m changing the effect I previously defined as return “.none” to “.run” and including the network request within it.
Even if it is not the best way to throw the request directly in this file, I am writing it here to explain it without complicating it. A network layer can be created and accessed in this run effect, but we will not do that now. We will use a network layer for API requests on the detail page. In effect, created in fetchBooks return, I trigger the booksFetched action via “send” after my network request is completed. This action assigns the data it receives to the variable I have in the state. So, why didn’t we update the state directly instead of calling a new action by writing “send(.booksFetched)”?
That’s because the state parameter is defined as “inout”; we cannot update an object accessed by reference as inout from within concurrently-executing code blocks.

Connect View with Reducer:

Now, when we call the fetchBooks action in BookListReducer, it will be able to pull the book list from the endpoint, and the booksFetched action will update the state. So, we can connect it to the BookListView.

Within the BookListView, I’ve created a variable called “store”. This variable holds the reducer. However, we cannot directly read data from this “store” variable or trigger any actions within the view. To update our view as this “store” changes, we need to use a structure called “WithViewStore”. This structure takes the “store” variable as a parameter and the scope we want to observe. Defining the observe scope as “{ $0 }” means it will observe all parameters in the State we created within the Reducer. We can also make it observe only specific variables. You can look at the performance section in the library documentation for more detailed information.

In the WithViewStore structure, we access the state parameters through the “ viewStore “ variable. I created a list and organized it to show the book list if the books variable in State is not nil. I call the “fetchBooks” action in the refresh button action through the viewStore. This action sends the API request, and when it receives a response from the API, it triggers the “booksFetched” action. This action updates the state. As the state is updated, our list is reloaded. And we can see the book list 🥳.

Character

Now, I can start creating the detail page. The detail page will list the characters in the selected book. So first, I create the “Character” model.

BookDetailReducer

Then, I created the BookDetailReducer. I created a variable in State that holds the Book value selected on the list page, a “characters” variable to preserve the character list and an isLoading variable to show a progressView when performing API requests. As an action, I added two actions named fetchCharacters and charactersFetched, as you will be familiar with from the List page. I added my actions in the Reduce function, but the fetchCharacters action has not thrown any API requests effect. I will do this in a BookClient layer since we will be making requests to multiple endpoints.

BookClient

I created a file named “BookClient” and defined a struct with the same name. This struct contains a single closure, “fetchCharacters,” which retrieves the links and returns an optional character array.

I also created an extension where I provided that the BookClient struct conforms to the DependencyKey protocol. I defined a static variable to confirm this protocol and included the “fetchCharacters” function. The “arrayRequest” function I used within this function is one that I created in an extension. This function takes a URL array, sends requests to all the URLs, and converts the responses into a character array. I didn’t include screenshots as it is not directly related to TCA, but you can find functions in the code repository.

To access the BookClient layer in the application through TCA’s Dependency Manager, I wrote an extension to the struct named DependencyValues and introduced BookClient. Now, I can access this struct in the project and send the character request.

I included “bookClient” as a dependency in “BookDetailReducer.” I restructured the “fetchCharacters” action within the reduce function to send a request to this client and trigger the “characterFetched” action upon receiving the response.

BookDetailReducer is ready, but we haven’t created the BookDetailView yet. Before creating the view, I will create a mock Book object to use in the preview.

BookDetailView

Now, we can create the BookDetailView. First, we create our store variable. We define a list in the WithViewStore structure to access the state and actions. If isLoading is true, the progressView will appear; if the data is loaded and there is a character array, the character list will appear; and if there is an error, the error text will appear.
In the onAppear of this view, we trigger the action to pull the character list. We can display the character list when we set the mock book object in the preview. However, we have not yet established a connection between BookListView and the detail page.

Navigation

We will redirect from the list page to the detail page using basic properties. You can check here to set up a complex navigation structure and see the other details.

To establish the connection between BookListView and BookDetailView, we start by editing BookListReducer. In state, we create a StackState named path. This variable will hold the states of the detail pages we will open. Then, we add a path action in the Action enum, which takes the State and Action enum of the detail page as parameters.
While making the page, we move the actions we defined in the reduce function into the ReduceBuilder, which we will call “body”. Because using this reduce builder gives us the flexibility we cannot do with the reduce function. We also add the path action inside the switch case structure; this action does not need to do anything for navigation.
At the end of the Reduce function, I call a forEach function. This function uses it to open all the pages attached to the path in TCA. As a parameter, it takes the path variable we created in State as keyPath and listens to the path action we added as an action. Since we will only open the detail page, I created just a BookDetailReducer object.

I’m removing the “NavigationView” that we previously used to make the NavigationBar visible, and I’m replacing it with “NavigationStackStore,” a customized view within TCA. “NavigationStackStore” is a specialized structure within TCA that holds states and actions.

To capture user clicks on the options from our book list and open the detail page, I added “NavigationLink” inside the “ForEach.” In essence, this is the SwiftUI NavigationLink we’re familiar with, but it’s called with a specialized init function that accepts a state. We create the state using the selected book and an object of type “BookDetailReducer.State.”
At the end of the view, I write the destination closure. This closure is invoked with the store parameter when a page needs to be opened. Since we only call the detail page, I call “BookDetailView” without customizing it with any store id.

BookListApp

Now, our list page and detail page are connected, and there is just one more step left to run the project on a simulator or iPhone. We need to call “BookListView” within the main app file. I created the store for our list page as a static variable. Then, we call our “BookListView”, where “ContentView” is called.

Useful links:

Thanks for reading. You can contact me for questions, suggestions, or feedback.

--

--