TCA, or The Composable Architecture, is a framework for iOS development that provides a structured and scalable approach to building robust, maintainable applications. Created by Brandon Williams and Stephen Celis, TCA leverages functional programming principles and Swift's powerful type system to offer a modern solution for iOS app architecture.

In this post, we’ll explore how to migrate our Rick and Morty iOS app to TC

The architecture

TCA consists of five main components:

  1. State: A single type that represents the entire state of an app or feature.
  2. Actions: An enumeration of all possible events that can occur in the app.
  3. Environment: A type that wraps all dependencies of the app or feature.
  4. Reducer: A function that transforms the current state to the next state based on a given action.
  5. Store: The runtime that powers the feature and manages the state.

TCA offers several advantages for iOS development:

  • Unidirectional data flow: This makes it easy to understand how changes in state occur, simplifying debugging and preventing unexpected side effects.
  • Improved testability: TCA encourages writing features that are testable by default.
  • Modularity: It allows for composing separate features, enabling developers to plan, build, and test each part of the app independently.
  • Scalability: TCA is particularly useful for complex applications with many states and interactions.

Configure XCode project

To get started with this architecture, integrate the ComposableArchitecture library from its GitHub repository.

Downloading might take some time.

Character feature

The component that we have to change implentation is basically the ViewModel component. In this case will be renamed as CharacterFeature.

import ComposableArchitecture

@Reducer
struct CharatersFeature {
    @ObservableState
    struct State: Equatable {
        var characters: [Character] = []
        var isLoading: Bool = false
    }

    enum Action {
        case fetchCharacters
        case fetchCharactersSuccess([Character])
    }

    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .fetchCharacters:
                state.isLoading = true
                state.characters = []
                return .run { send in
                    let result = await currentApp.dataManager.fetchCharacters(CharacterService())
                    switch result {
                    case .success(let characters):
                        //state.characters = characters
                        await send(.fetchCharactersSuccess(characters))
                    case .failure(let error):
                        print(error)
                    }
                }
            case .fetchCharactersSuccess(let characters):
                state.isLoading = false
                state.characters = characters
                return .none
            }
        }
    }
}

This code defines a feature using the Composable Architecture (TCA) framework in Swift. Let’s break down what this code does:

  1. Import and Structure:
    • It imports the ComposableArchitecture framework.
    • It defines a CharatersFeature struct with the @Reducer attribute, indicating it’s a reducer in the TCA pattern.
  2. State:
    • The State struct is marked with @ObservableState, making it observable for SwiftUI views.
    • It contains two properties:
      • characters: An array of Character objects.
      • isLoading: A boolean to track if data is being loaded.
  3. Actions:
    • The Action enum defines two possible actions:
      • fetchCharacters: Triggers the character fetching process.
      • fetchCharactersSuccess: Handles successful character fetching.
  4. Reducer:
    • The body property defines the reducer logic.
    • It uses a Reduce closure to handle state changes based on actions.
  5. Action Handling:
    • For .fetchCharacters:
      • Sets isLoading to true and clears the characters array.
      • Runs an asynchronous operation to fetch characters.
      • On success, it dispatches a .fetchCharactersSuccess action.
      • On failure, it prints the error.
    • For .fetchCharactersSuccess:
      • Sets isLoading to false.
      • Updates the characters array with the fetched data.
  6. Asynchronous Operations:
    • It uses .run for handling asynchronous operations within the reducer.
    • The character fetching is done using currentApp.dataManager.fetchCharacters(CharacterService()).

This code essentially sets up a state management system for fetching and storing character data, with loading state handling. It’s designed to work with SwiftUI and the Composable Architecture, providing a structured way to manage application state and side effects.

View

The view is almost the same as before:

struct CharacterView: View {
    let store: StoreOf<CharatersFeature>
    
    var body: some View {
        NavigationView {
            ZStack {
                if store.isLoading {
                    ProgressView()
                }
            ScrollView {
                    ForEach(store.characters) { character in
                        NavigationLink {
                            DetailView(character: character)
                        } label: {
                            HStack {
                                characterImageView(character.imageUrl)
                                Text("\(character.name)")
                                Spacer()
                            }
                        }
                    }
                }
            }
        }
        .padding()
        .onAppear {
            store.send(.fetchCharacters)
        }
    }

The store holds observable items used by the view to present either the progression view or the character list. When the view appears, it triggers the .fetchCharacters action, prompting the reducer to fetch the character list.

Unit test

Unit testing with TCA differs significantly from my expectations:

    @Test func example() async throws {
        // Write your test here and use APIs like `#expect(...)` to check expected conditions.
        let store = await TestStore(initialState: CharatersFeature.State()) {
            CharatersFeature()
        }
        
        await store.send(.fetchCharacters) {
          $0.isLoading = true
        }
        
        await store.receive(\.fetchCharactersSuccess, timeout: .seconds(1)) {
          $0.isLoading = false
            $0.characters = expCharacters
        }
        
        await store.finish()
    }

In TCA, testing often focuses on asserting the state transitions and effects of the reducer. Instead of traditional XCTest assertions like XCTAssertEqual, TCA provides its own mechanism for testing reducers using TestStore, which is a utility designed to test state changes, actions, and effects in a deterministic way.

Conclusions

This is a very minimalistic example just to get in touch with this architecture. With more complex applications, I meain with some flows and many screens reducer would become a huge chunk of code, so god approach would be implement this pattern per app flow.You can find source code used for writing this post in following repository.

Copyright © 2024-2025 JaviOS. All rights reserved