Skip to main content

Quick start

This guide will walk you through building a simple counter feature with TCA. You’ll learn how to manage state, handle actions, execute side effects, and write tests.
For a more in-depth, interactive tutorial, check out Meet the Composable Architecture.

What you’ll build

You’ll build a feature that displays a number along with increment and decrement buttons. It will also have a button that fetches a random fact about the current number from an API and displays it.

Step 1: Create the reducer

First, create a new type annotated with the @Reducer macro. This will house the domain and behavior of your feature.
import ComposableArchitecture

@Reducer
struct Feature {
}

Step 2: Define the state

Inside your reducer, define a State type that describes all the data your feature needs. Apply the @ObservableState macro to take advantage of Swift’s observation tools.
@Reducer
struct Feature {
  @ObservableState
  struct State: Equatable {
    var count = 0
    var numberFact: String?
  }
}
The state contains:
  • count: The current number to display
  • numberFact: An optional string for the fetched fact

Step 3: Define actions

Define an Action enum that represents all the actions that can happen in your feature.
@Reducer
struct Feature {
  @ObservableState
  struct State: Equatable {
    var count = 0
    var numberFact: String?
  }
  
  enum Action {
    case decrementButtonTapped
    case incrementButtonTapped
    case numberFactButtonTapped
    case numberFactResponse(String)
  }
}
The actions include:
  • User interactions: increment, decrement, and fact button taps
  • System events: receiving the fact response from the API

Step 4: Implement the reducer

Implement the body property to describe how state evolves when actions occur.
@Reducer
struct Feature {
  @ObservableState
  struct State: Equatable { /* ... */ }
  
  enum Action { /* ... */ }
  
  var body: some Reducer<State, Action> {
    Reduce { state, action in
      switch action {
      case .decrementButtonTapped:
        state.count -= 1
        return .none
        
      case .incrementButtonTapped:
        state.count += 1
        return .none
        
      case .numberFactButtonTapped:
        return .run { [count = state.count] send in
          let (data, _) = try await URLSession.shared.data(
            from: URL(string: "http://numbersapi.com/\(count)/trivia")!
          )
          await send(
            .numberFactResponse(String(decoding: data, as: UTF8.self))
          )
        }
        
      case let .numberFactResponse(fact):
        state.numberFact = fact
        return .none
      }
    }
  }
}
Actions that don’t need to execute side effects return .none. The fact button returns a .run effect that performs an asynchronous API request.

Step 5: Build the view

Create a SwiftUI view that holds a StoreOf<Feature> to observe state and send actions.
struct FeatureView: View {
  let store: StoreOf<Feature>
  
  var body: some View {
    Form {
      Section {
        Text("\(store.count)")
        Button("Decrement") { store.send(.decrementButtonTapped) }
        Button("Increment") { store.send(.incrementButtonTapped) }
      }
      
      Section {
        Button("Number fact") { store.send(.numberFactButtonTapped) }
      }
      
      if let fact = store.numberFact {
        Text(fact)
      }
    }
  }
}

Step 6: Create the store

At your app’s entry point, construct a store with initial state and the reducer.
import ComposableArchitecture

@main
struct MyApp: App {
  var body: some Scene {
    WindowGroup {
      FeatureView(
        store: Store(initialState: Feature.State()) {
          Feature()
        }
      )
    }
  }
}

Step 7: Test your feature

TCA makes testing straightforward. Use TestStore to assert how your feature evolves over time.
import ComposableArchitecture
import XCTest

@Test
func basics() async {
  let store = TestStore(initialState: Feature.State()) {
    Feature()
  } withDependencies: {
    $0.numberFact.fetch = { "\($0) is a good number Brent" }
  }
  
  // Test increment
  await store.send(.incrementButtonTapped) {
    $0.count = 1
  }
  
  // Test decrement
  await store.send(.decrementButtonTapped) {
    $0.count = 0
  }
  
  // Test fetching fact
  await store.send(.numberFactButtonTapped)
  
  await store.receive(\.numberFactResponse) {
    $0.numberFact = "0 is a good number Brent"
  }
}
The test above won’t compile yet because we haven’t extracted the number fact dependency. Continue to the next step to fix this.

Step 8: Extract dependencies

To make your feature testable, extract the API dependency so you can control it in tests.
1

Create a dependency client

struct NumberFactClient {
  var fetch: (Int) async throws -> String
}
2

Register with the dependency system

extension NumberFactClient: DependencyKey {
  static let liveValue = Self(
    fetch: { number in
      let (data, _) = try await URLSession.shared.data(
        from: URL(string: "http://numbersapi.com/\(number)")!
      )
      return String(decoding: data, as: UTF8.self)
    }
  )
}

extension DependencyValues {
  var numberFact: NumberFactClient {
    get { self[NumberFactClient.self] }
    set { self[NumberFactClient.self] = newValue }
  }
}
3

Use the dependency in your reducer

@Reducer
struct Feature {
  @Dependency(\.numberFact) var numberFact
  
  // ...
  
  var body: some Reducer<State, Action> {
    Reduce { state, action in
      switch action {
      // ...
      
      case .numberFactButtonTapped:
        return .run { [count = state.count] send in
          let fact = try await self.numberFact.fetch(count)
          await send(.numberFactResponse(fact))
        }
      
      // ...
      }
    }
  }
}
Now your test will compile and run! The test dependency is automatically used in tests, while the live dependency runs in the app.

What you learned

In this quick start guide, you:

Defined state

Created a state type with @ObservableState to hold feature data

Handled actions

Defined an action enum for user interactions and system events

Implemented logic

Used the Reduce reducer to evolve state based on actions

Executed effects

Used .run to perform asynchronous API requests

Built the UI

Created views that observe the store and send actions

Wrote tests

Used TestStore to verify feature behavior with mocked dependencies

Next steps

You’ve built your first TCA feature! Here’s what to explore next:

Navigation

Learn how to navigate between features and manage navigation state

Composition

Break down large features into smaller, reusable components

Testing

Deep dive into testing strategies and best practices

Examples

Explore real-world example applications built with TCA

Build docs developers (and LLMs) love