TCA examples often show a counter. Its value appears when one screen loads local data, refreshes an API, starts a cancellable stream, presents a child feature, and handles retries.
@Reducer
struct ChatFeature {
@ObservableState
struct State: Equatable {
var messages: [Message] = []
var input = ""
var isStreaming = false
var error: AppError?
}
enum Action {
case sendTapped
case streamResponse(Result<ChatEvent, AppError>)
case cancelTapped
}
}
State contains everything that can affect rendering. Actions describe events, not setter methods. The reducer decides the next state and which effects begin.
That gives debugging a useful question: “Which action produced this state?” Network callbacks and view lifecycle events enter through the same vocabulary.
Effects must have identities when newer work replaces older work. A chat stream, search request, or session load should be cancellable without cancelling unrelated tasks.
TCA does add types and ceremony. I use it where a feature has concurrent effects, navigation state, retry behavior, or several child flows. A static settings screen may not need the same weight.
The goal is not to put every line in a reducer. Formatting and pure calculations remain ordinary Swift functions. Persistence and APIs remain dependencies. The reducer owns coordination: the part most likely to become implicit and untestable in a large SwiftUI view.