Home Articles

Getting Started with The Composable Architecture (TCA)

Basics of Swift — The Composable Architecuture

Demo of Swift — The Composable Architecuture

This article will help in understanding the Swift—Composable Architecture and how it benefits SwiftUI Apps efficiently.

Overview of TCA Brendan Williams and Stephen Celis are the brilliant minds behind TCA, showcasing their innovation in software architecture.

Drawing inspiration from the well-regarded paradigms of ELM and Redux, TCA has firmly established itself as a notable player in the realm of software development.

Now, let’s dive into TCA’s key offerings, which comprise

  1. an Opinionated Library tailored for app development
  2. a Multi-Store Architecture
  3. a Unidirectional Flow pattern
  4. an efficient method for managing Side Effects.

The term ‘Composition’ itself hints at the process of breaking down complex functionalities into smaller, modular units that can be easily combined. Reducer builders and operators, like Scope, offer a robust solution for handling complex features and promoting modularization within software projects

ELM Architecture Flow

The architecture flow of ELM, a functional programming framework for web applications comprises View, Update, and Model stages. Model stores app state, Update handles data changes via messages, and View produces the UI based on the current state.

REDUX Architecture

On the other hand, we have a popular state management pattern for JavaScript applications.

Redux introduces a centralized store to manage the application’s state. The process begins with the Action phase — Actions describe state changes. Reducers define how the state changes based on actions. The Store stores the state. After the Redux cycle, integrating View with React, where components dispatch actions to change the state.

Framework Integration In terms of Framework Integration to our Xcode project, the TCA has supported only Swift Package Manager (SPM).

They have started building this framework in the year 2019 and released their first version on May 4th 2020. Open source framework and also supported widely by the community.

To Integrate the framework, we need to add packages to our project and add their repo URL to get the packages.

Latest Version — 1.2.0

Open source and supported widely by the community

To install the SDK through Swift Package Manager:

In Xcode, select File > Add Package and enter https://github.com/pointfreeco/swift-composable-architecture as the repository URL

TCA is a little more Opinionated library compared to ELM and Redux architectures. Side Effect: Event modifies App State from outside or External Change. Hence the Debugging will be tough.

State Management: Behaves for different state value

Composition: Divide and Conquer — well-defined, isolated modules

Testing: Testing the mini modules

Ergonomics — Framework Provides convenient APIs to implement the components

Before we dive into the TCA flow, let’s establish some fundamental concepts of Swift UI.

SwiftUI — Declarative UI As we are well acquainted, SwiftUI is built upon the Declarative UI paradigm.

In UIKit, there exists a disparity between the App State and the View State. These can be altered independently, leading to a loss of synchronization.

App State is inconsistent with View State Losing Synchronization However, in the realm of SwiftUI, the App State remains consistent and synchronized.

Synchronization App State is consistent State rules everything UI = fn(State)

SwiftUI

Drawing from the SwiftUI Data Model presented at WWDC 2019, it becomes evident that Views are a result of state, rather than a sequence of events.

When users engage with the app through actions, the state undergoes mutation.

Following this transformation, the state update triggers a need for a View update, directly tied to the state’s alterations.

This process guides the View to generate an updated version of the UI, aligned with the current state.

Unidirectional Flow

The powerful unidirectional flow adopted by The Composable Architecture (TCA) in Swift. TCA’s implementation of unidirectional flow simplifies app development, making it more predictable and easier.

The unidirectional flow in TCA begins with the Action phase. These actions represent user interactions, such as button presses or text input.

Let’s discuss about Unidirectional Flow

Unidirectional Flow

The Composable Architecture adopts the unidirectional flow.

Unidirectional Flow is the way to mutate the State, we need to send Actions to the ViewStore which is also called a runtime store.

The store holds all business logic and is responsible for mutating.

Reducer comes into the picture and handles action which is the boilerplate.

Unidirectional Flow - Collection

In TCA, these flows show how state and action are chained together and how they flow between single View and collection View

Since TCA allows unidirectional data flow, you can always access the child state from the parent reducer.

Deep Dive into TCA Architecture

TCA Flow

View sends the action can be of any events such as onAppear or fetch API service data

Reducer function is the pure function taking the current state and action as input, producing a new state. This is called a Mutation. This flow happens in the unidirectional flow with the help of Store.

Following the transformations, the state update triggers a need for a View update.

State represented as a structure, Action as enums, Reducer — pure function. This Unidirectional Flow happens in the runtime store that is called a Store.

Building Blocks

State represents the entire UI state of a feature/module. Single source of truth for the UI

Action represents user intents or events in a declarative manner You can take examples such as Examples: Button tapped, data fetched, text entered

Reducer is the heart of TCA — responsible for state transitions. Pure function takes the current state and action as input, producing a new state.

Effect represents side effects, like asynchronous tasks (network requests, etc.)

Store manages the state, actions, and reducers. As TCA adopts the unidirectional data flow, Actions trigger state changes through reducers.

Dependencies required for actions and effects

Let’s take a look at the implementation part. Here we will use the CurrencyConverter App.

State

State is a value type that describes the entire UI state of a feature/module. It’s a single source of truth for the UI. Changes in the UI are a result of changes in the state. Must needed for logic and UI. This provides the application’s initial state and a collection of properties.

A single way to mutate the state by dispatching actions to the store which holds the state

State represented as Structure and it conforms to the Equatable protocol. You can see this as an example with a property.


    struct State: Equatable {
            var initialCurrencies: Currencies = []
            var currencies: Currencies = []
            var priceQuantityEntered = "1"
            var selectedBaseCurrency: String = "EUR"
     }

Action

Actions represent user intents or events in a declarative manner. Value types that describe what the user wants or what has happened. Examples: Button tapped, data fetched, text entered.


    enum Action: Equatable {
            case onAppear
            case processAPIResponse(CurrencyData)
            case quantityTextFieldEntered(String)
            case countryCodePickerSelected(String)
            case updateCurrencies(Currencies)
     }
 

Reducer

Reducer is the heart of TCA and pure function. Reduce into function is the more primitive version that involves implementing all of the logic for the feature directly in this method. Pure Function transforms the current state to the next state and it determines how the state changes based on the current action

It is also responsible for returning any Effects that should be run such as API requests by returning an Effect value.


    var body: some ReducerOf<Self> {
            Reduce{ state, action in
                switch action {
                case .onAppear:
                    return .run { send in
                        let (data, _) = try await URLSession.shared.data(from: URL(string: "https://pastebin.com/raw/Nq1KvHjZ")!)
                        let currencyData = try JSONDecoder().decode(CurrencyData.self, from: data)
                        await send(.processAPIResponse(currencyData))
                    }
                case let .processAPIResponse(data):
                    let cuurencies = getTableViewDataArray(currencyListView: data)
                    state.initialCurrencies = cuurencies
                    return .send(.updateCurrencies(cuurencies))
                case let .updateCurrencies(currencies):
                    state.currencies = currencies
                    return .none
                case let .quantityTextFieldEntered(string):
                    state.priceQuantityEntered = string
                    print(string)
                    return .send(.updateCurrencies(reactToEnteredAmount(state: state, amount: Double(string) ?? 1.0)))
                case let .countryCodePickerSelected(currencyCode):
                    state.selectedBaseCurrency = currencyCode
                    return .none
                }
            }
        }
   

The body style of defining a reducer is more “high level”. You don’t directly perform any of the mutation or effect logic in the body and instead, you compose many other reducers together, similar to how SwiftUI views work.

Let’s move into the View Part


    import SwiftUI
    import ComposableArchitecture
    
    

Store

Store is a runtime that drives the feature. It sends all user actions to the store so that it can run the reducer and effects, to observe state changes in the store. From this, we can able to update the UI.

Operators such as:

Scope — break stores into smaller ones

Combine — combine two or more reducers into a single reducer

WithViewStore

Initializes a structure that transforms a “Store” into an observable ViewStore in order to compute views from the state.


    let store: StoreOf<CurrencyConverter>
    
    StoreOf<R: Reducer> = Store<R.State, R.Action>

    WithViewStore(self.store, observe: { $0 }) { viewStore in
    }

Observe -> Pass a closure that describes what state you want to observe. This gives a trailing closure and hands a ViewStore


    var body: some View {
            WithViewStore(self.store, observe: { $0 }) { viewStore in
                VStack {
                    HStack {
                        TextField(
                            "Enter text",
                            text: viewStore.binding(
                                get: \.priceQuantityEntered,
                                send: { .quantityTextFieldEntered($0) }
                            )
                        )
                        .keyboardType(.numberPad)
                        .textFieldStyle(.roundedBorder)
                        .padding()
                        Spacer()
                        Picker("Currency",
                               selection: viewStore.binding(
                                get: \.selectedBaseCurrency,
                                send: { .countryCodePickerSelected($0) })
                        ) {
                            ForEach(viewStore.currencies) {
                                Text($0.currencyCode)
                            }
                        }
                        .pickerStyle(.menu)
                    }
                    Spacer()
                    List(viewStore.currencies) { tableViewData in
                        CurrencyRowView(tableViewData: tableViewData)
                    }
                }
                .onAppear {
                    viewStore.send(.onAppear)
                }
            }
        }
        

I hope you like this overview of The Composable Architecture (TCA). There are other benefits such as NavigationStack, HigherOrderFunctions, and Effects with Timers.

Additionally, it also supports Concurrency, Testing and domain-specific languages.

Effect

Effect — Decoupled from reducers to keep logic clean and testable. The effect can result in the Reducer function. Dispatched by reducers, run asynchronously, and can produce new actions. Effect represented as a struct. The reducer function returns to the effect.


    ##Declaration
    struct Effect<Action>
    
    ##Implementation:
    func reduce(into state: inout State, action: Action) -> Effect<Action> { 
     }
 

Environment

Environment — Provides dependencies required for actions and effects. Example — API Clients, random number generators.

Makes testing easier by injecting mock dependencies.


    @Dependency(\.continuousClock) var clock
    

Comparison with other architectures

  1. The era of Declarative UI (SwiftUI) — Composable architecture is required
  2. Easy to make UI components and combine/compose the components
  3. The benefits of Declarative UI can be maximized.
  4. Multi-Platform — Supports SwiftUI on iOS, WatchOS, MacOS
  5. Modularity in working with teams and reusing the modules
  6. Pure Function Reducer — low boilerplate compared to VIPER
  7. Consistency and Testable compared to MVVM + Coordinator
  8. Protected against unexpected side effects and handling

Benefits

  1. Isolation — Isolated local stores and states
  2. Composition — Composition of mini-Modules
  3. Pure Functions — Uses pure functions(Reducer Functions) to update the State
  4. Managing side effects — Functions are executed by the runtime system and produce new actions

Shortcoming

  1. Learning about composable architecture can be challenging for new developers due to its steep learning curve.
  2. Frequent changes in the library require preparation for implementation in the project.
  3. Complexity in Integration and not the drag-and-drop solution.

The entire project can be found here

In September 2023, I gave a speech about this topic at a SwiftBlr meetup.

This is a free third party commenting service we are using for you, which needs you to sign in to post a comment, but the good bit is you can stay anonymous while commenting.