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
orcounterButtonTapped
. -
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()
}
))
)
}
}
}
Be the first to comment