Skip to content

Latest commit

 

History

History
380 lines (290 loc) · 8.57 KB

File metadata and controls

380 lines (290 loc) · 8.57 KB

TCA (The Composable Architecture) Architecture

Overview

The TCA implementation demonstrates a functional architecture pattern with unidirectional data flow, emphasizing composition, testing, and predictability. It provides a structured approach to managing state and side effects through explicit actions and pure reducers.

Architecture Diagram

graph TB
    subgraph "View Layer"
        V1[TCAContentView]
        V2[TCASidebarView]
        V3[TCAContentListView]
        V4[TCADetailView]
        V5[TCASettingsContentView]
    end

    subgraph "Feature Layer (Reducer)"
        F1["TCAAppFeature
        @Reducer"]
        F2["State
        @ObservableState"]
        F3["Action
        enum"]
        F4["Reducer
        body"]
    end

    subgraph "Store"
        S1["Store
        Runtime"]
    end

    subgraph "Model Layer"
        M1["TCAListItem
        struct"]
        M2["TCASidebarCategory
        enum"]
    end

    V1 -- "reads/sends" --> S1
    V2 -- "reads/sends" --> S1
    V3 -- "reads/sends" --> S1
    V4 -- "reads" --> S1
    V5 -- "reads/sends" --> S1

    S1 -- "holds" --> F2
    S1 -- "accepts" --> F3
    S1 -- "executes" --> F4

    F4 -- "transforms" --> F2
    F4 -- "uses" --> M1
    F4 -- "uses" --> M2

    F2 -- "contains" --> M1
    F2 -- "references" --> M2

    style F1 fill:#e1f5ff
    style F2 fill:#d4edda
    style F3 fill:#fff3cd
    style F4 fill:#f8d7da
    style S1 fill:#e7e7ff
    style M1 fill:#fff4e1
    style M2 fill:#fff4e1
Loading

When to Use TCA

Use TCA when:

  • Building complex applications with lots of state
  • Maximum testability is critical
  • You want composable, reusable features
  • Team embraces functional programming
  • Need time-travel debugging
  • Want explicit, predictable state changes
  • Building for long-term maintainability

Consider alternatives when:

  • Building simple apps or prototypes
  • Team unfamiliar with functional programming
  • Can't add external dependencies
  • Want minimal boilerplate
  • Need rapid prototyping speed

Core Concepts

1. State

The single source of truth for your feature:

@ObservableState
struct State: Equatable {
    var selectedCategory: TCASidebarCategory? = .category1
    var selectedItem: TCAListItem? = nil
    var category1Items: [TCAListItem] = []
}

Characteristics:

  • Struct (value type) for immutability
  • Equatable for change detection
  • @ObservableState for SwiftUI integration
  • Contains ALL data the feature needs

2. Action

An enum describing every way state can change:

enum Action: Equatable {
    case categorySelected(TCASidebarCategory?)
    case itemSelected(TCAListItem?)
    case addItemTapped
    case deleteItem(TCAListItem)
}

Characteristics:

  • Enum cases represent events
  • Associated values carry data
  • Past tense naming (what happened)
  • Equatable for testing

3. Reducer

A pure function that evolves state:

var body: some Reducer<State, Action> {
    Reduce { state, action in
        switch action {
        case let .categorySelected(category):
            state.selectedCategory = category
            state.selectedItem = nil
            return .none
        }
    }
}

Characteristics:

  • Pure function (no side effects)
  • Mutates state parameter
  • Returns effects to execute
  • Testable in isolation

4. Store

The runtime that powers the feature:

let store = Store(initialState: TCAAppFeature.State()) {
    TCAAppFeature()
}

Characteristics:

  • Holds current state
  • Accepts action dispatches
  • Runs reducer
  • Executes effects
  • Notifies views of changes

Unidirectional Data Flow

sequenceDiagram
    actor User
    participant View
    participant Store
    participant Reducer
    participant State

    User->>View: Taps "Add Item"
    View->>Store: send(.addItemTapped)
    Store->>Reducer: reducer(state, .addItemTapped)
    Reducer->>State: Mutate state
    Reducer-->>Store: Return Effect (.none)
    Store-->>View: State changed notification
    View-->>User: UI updates automatically
Loading

File Structure

Examples/TCA/
├── Models/                        # Pure data structures
│   ├── TCASidebarCategory.swift   # Category enum
│   └── TCAListItem.swift          # Item struct
│
├── Features/                      # State + Actions + Reducers
│   └── TCAAppFeature.swift        # Main feature definition
│
└── Views/                         # SwiftUI views
    ├── TCAContentView.swift       # Root view
    ├── TCASidebarView.swift       # Sidebar (first pane)
    ├── TCAContentListView.swift   # Content list (second pane)
    ├── TCADetailView.swift        # Detail (third pane)
    └── TCASettingsContentView.swift # Settings form

Layer Details

1. Models (Data)

Purpose: Pure data structures

struct TCAListItem: Identifiable, Hashable, Equatable {
    let id = UUID()
    let title: String
    let subtitle: String
    // ... more properties
}

Characteristics:

  • Immutable (let)
  • Equatable for state comparison
  • No business logic

2. Feature (State + Actions + Reducer)

Purpose: Define feature behavior

@Reducer
struct TCAAppFeature {
    @ObservableState
    struct State: Equatable { /* ... */ }

    enum Action: Equatable { /* ... */ }

    var body: some Reducer<State, Action> {
        Reduce { state, action in
            // Transform state based on action
        }
    }
}

Characteristics:

  • @Reducer macro for composition
  • State is observable
  • Actions are equatable
  • Reducer is pure function

3. Views (UI)

Purpose: Display state and send actions

struct TCAContentView: View {
    let store: StoreOf<TCAAppFeature>

    var body: some View {
        WithPerceptionTracking {
            // UI that reads store.state
            Button("Add") {
                store.send(.addItemTapped)
            }
        }
    }
}

Characteristics:

  • WithPerceptionTracking for observation
  • Read state via store.state
  • Send actions via store.send()
  • No business logic

Testing

TCA makes testing straightforward with TestStore:

func testAddItem() async {
    let store = TestStore(initialState: TCAAppFeature.State()) {
        TCAAppFeature()
    }

    await store.send(.addItemTapped) {
        $0.category1Items.append(/* expected item */)
    }
}

Benefits

1. Predictability

All state changes are explicit through actions:

store.send(.categorySelected(.category1))

2. Testability

Every feature is fully testable:

let store = TestStore(/* ... */)
await store.send(.action)

3. Composability

Features can be broken down and composed:

Scope(state: \.sidebar, action: \.sidebar) {
    SidebarFeature()
}

4. Debugging

Action log shows complete history:

Action: categorySelected(.category1)
Action: itemSelected(item)
Action: addItemTapped

Comparison with Other Patterns

Aspect TCA Redux MVVM VIPER
Complexity High Medium Medium High
Testability Excellent Great Good Excellent
Learning Curve Steep Medium Medium Steep
Boilerplate High Medium Low High
Effects System Built-in Manual Manual Manual
Composition Excellent Good Good Good
Dependencies Required Optional None None

Best Practices

  1. Keep State Minimal - Only store what's necessary
  2. Use Equatable - Makes change detection efficient
  3. Pure Reducers - No side effects in reducer body
  4. Test Everything - TCA makes it easy
  5. Compose Features - Break large features down
  6. Document Actions - Clear names explain intent
  7. Use Effects - All async work through effects

Learning Resources

Summary

TCA provides a comprehensive architecture with:

  • ✅ Unidirectional data flow
  • ✅ Complete testability
  • ✅ Composable features
  • ✅ Built-in effects system
  • ✅ Time-travel debugging
  • ✅ Type safety

Trade-off: More complexity and boilerplate than simpler patterns.

Recommendation: Use for complex apps where testability and predictability are critical priorities.