The Composable Architecture
Introduction to TCA
The Composable Architecture
Introduction to TCA
0
0
Checkbox to mark video as read
Mark as read

The Composable Architecture (TCA) is a modern architecture pattern for building applications in Swift. It’s designed to make SwiftUI development modular, testable, and scalable, addressing common challenges in state management and app organization. Created by Point-Free, TCA brings together Redux-style unidirectional data flow, modularity, and Swift’s type-safety to help developers manage complex app logic with clarity.

Check the offical page here.

Understanding TCA: The Core Principles

TCA’s foundation is based on a few key principles: a single source of truth for state, unidirectional data flow, and modularity. This architecture pattern encourages developers to organize their apps around four core concepts:

  • State: A struct that holds all the data needed to render a part of your app.
  • Action: An enum that defines all the events that can affect the state.
  • Environment: A structure holding dependencies, such as API clients or database connections.
  • Reducer: A function that takes in the current state and an action, and returns the new state based on the action.


Using TCA

To start using TCA, add The Composable Architecture library to your project using the Swift Package Manager. The library provides you with the essential tools to build out your app following TCA principles.

Create the Reducer

The Reducer will manage the user Actions from our View and will change the State accordingly.

import ComposableArchitecture

@Reducer
struct ContentViewReducer {

    @ObservableState
    struct State {
        var counter = 0
    }

    enum Action {
        case onAppear
        case counterButtonTapped
    }

    var body: some ReducerOf {
        Reduce { state, action in
            switch action {
            case .onAppear:
                return .none

            case .counterButtonTapped:
                state.counter += 1
                return .none
            }
        }
    }
}

Let's analyze ContentViewReducer in detail:

  • State will just have the data we need to build our view, we'll see how later.
  • Action is the collection of actions that we'll get from the view. We can add here actions like onAppear or counterButtonTapped.
  • body is the implementation of the Reducer protocol that we conform when we use @Reducer macro provided by the TCA library. Here is where we'll process the actions sent from the View, returning .none at the end of each case for now.

Use the Reducer from the View

To communicate the View and the Reducer, we need to declare a Store. Then we'll we able to access our State variables using this new object, as well as sending the actions we defined:

import SwiftUI
import ComposableArchitecture

struct ContentView: View {

    let store: StoreOf<ContentViewReducer>

    var body: some View {
        Text("Count: \(store.counter)")

        Button {
            store.send(.counterButtonTapped)
        } label: {
            Text("Press This Button")
        }

    }
}

#Preview {
    ContentView(
        store: Store(
            initialState: ContentViewReducer.State(),
            reducer: {
                ContentViewReducer()
            }
        )
    )
}

As you can see in the Preview, now we need to inject the store creating a new one that will get an State and a Reducer as parameters. So we'll do the same in the main App file:

import SwiftUI
import ComposableArchitecture

@main
struct EducaSwiftApp: App {

    var body: some Scene {
        WindowGroup {
            ContentView(
                store: Store(
                    initialState: ContentViewReducer.State(),
                    reducer: {
                        ContentViewReducer()
                    }
                )
            )
        }
    }
}

Binding variables

In order to enable State binding, we need to declare our Store as @Binding variable. Then any variable in our State will be Bindable. We also need to declare an action for each of our binding actions:

import ComposableArchitecture

@Reducer
struct ContentViewReducer {

    @ObservableState
    struct State {
        var inputText: String = ""
    }

    enum Action {
        case inputTextChanged(String)
    }

    var body: some ReducerOf {
        Reduce { state, action in
            switch action {

            case .inputTextChanged(let newValue):
                state.inputText = newValue
                return .none
            }
        }
    }
}

import SwiftUI
import ComposableArchitecture

struct ContentView: View {

    @Binding var store: StoreOf

    var body: some View {

        Text("Keyboard: \(store.inputText)")

        TextField("Write here...", text: $store.inputText.sending(\.inputTextChanged))
    }
}

When the TextField changes its value, an inputTextChanged action will be sent to the Reducer, where we can update inputText value, as well as doing any other custom action.

The initialisation of our ContentView will change, requiring a Bindable parameter. For this example we can define just a .constant bindable Store:

import SwiftUI
import ComposableArchitecture

@main
struct EducaSwiftApp: App {

    var body: some Scene {
        WindowGroup {
            ContentView(
                store: .constant(Store(
                    initialState: ContentViewReducer.State(),
                    reducer: {
                        ContentViewReducer()
                    }
                ))
            )
        }
    }
}

0 Comments

Join the community to comment

Be the first to comment

Accept Cookies

We use cookies to collect and analyze information on site performance and usage, in order to provide you with better service.

Check our Privacy Policy