ReducerProtocol in The Composable Architecture (TCA)
Evangelist Apps
?? We craft mobile apps, full-stack web solutions & AI-powered innovations to accelerate business and digital success!
By Atikur Rahman , Lead iOS Developer at Evangelist Apps
At?Evangelist Apps, we have been using The Composable Architecture (TCA) for building complex production iOS apps for the last couple of years or so. In our opinion, TCA is the?Best Suited Architecture For SwiftUI. In this article, we will discuss about?ReducerProtocol, which is a new way to create?reducers?in TCA.
ReducerProtocol?is a protocol that describes how to evolve the current state of an application to the next state, given an action, and describes what effects should be executed later by the store, if any.
By conforming types to the?ReducerProtocol, we can:
In order to explain things better, let’s build a simple app using TCA. We will build two versions of the same app — the first version will be without using the?ReducerProtocol. Next, we will build the same app using?ReducerProtocol.
Our demo app will contain just a single feature — adding todo items & displaying them as a list.
Let’s define the?State?for our feature. The state is usually defined as a struct, containing properties that store the relevant data. In our case, we need to keep track of two pieces of data —
struct MyTodosState: Equatable {
var items = [String]()
var newItemTitle = ""
}
Next, we add?Action?type.?Action?in TCA is a type that represents the events that can happen in our specific feature. The action is usually defined as an?enum, containing cases that describe the possible events. Here, we add two cases to handle to events -
enum MyTodosAction: Equatable {
case newItemTitleChanged(String)
case addNewItem
}
Then we add?Reducer. Reducer in TCA is a function that describes how to evolve the current state of the app to the next state given an action. The reducer is also responsible for returning any effects that should be run, such as API requests, which can be done by returning an?Effect?value.
let appReducer = Reducer<MyTodosState, MyTodosAction, Void> { state, action, _ in
switch action {
case .newItemTitleChanged(let title):
state.newItemTitle = title
return .none
case .addNewItem:
state.items.append(state.newItemTitle)
state.newItemTitle = ""
return .none
}
}
Next, we add?View?as follows, which represents a simple UI for adding a new todo item and displaying existing items -
import SwiftUI
import ComposableArchitecture
struct MyTodosView: View {
let store: Store<MyTodosState, MyTodosAction>
var body: some View {
WithViewStore(store) { viewStore in
VStack {
TextField(
"Add New Todo Item",
text: viewStore.binding(
get: \.newItemTitle,
send: MyTodosAction.newItemTitleChanged
)
)
Button("Add") { viewStore.send(.addNewItem) }
List(viewStore.items, id: \.self) {
Text($0)
}
}
.padding(20)
}
}
}
Finally, we create a?Store?to hold the current state & send actions to update the state.
领英推荐
import SwiftUI
import ComposableArchitecture
@main
struct MyTestApp: App {
var body: some Scene {
WindowGroup {
MyTodosView(
store: Store(
initialState: MyTodosState(),
reducer: appReducer,
environment: ()
)
)
}
}
}
Now we will see how we can use?ReducerProtocol. We will create a new type that will house the domain and behavior of the feature by conforming to?ReducerProtocol. Let’s call our feature?MyTodos.
import ComposableArchitecture
struct MyTodos: ReducerProtocol {
}
Here, we will add a type for our feature’s?State?and another type for the feature’s?Action?-
struct MyTodos: ReducerProtocol {
struct State: Equatable {
var items = [String]()
var newItemTitle = ""
}
enum Action: Equatable {
case newItemTitleChanged(String)
case addNewItem
}
}
Finally, we need to implement the?reduce?method, where we handle the logic and behavior for the feature. So, this is final implementaion of?MyTodos?feature, which conforms to?ReducerProtocol.
struct MyTodos: ReducerProtocol {
struct State: Equatable {
var items = [String]()
var newItemTitle = ""
}
enum Action: Equatable {
case newItemTitleChanged(String)
case addNewItem
}
func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
switch action {
case .newItemTitleChanged(let title):
state.newItemTitle = title
return .none
case .addNewItem:
state.items.append(state.newItemTitle)
state.newItemTitle = ""
return .none
}
}
}
Let’s also update the?MyTodosView?implementation -
import SwiftUI
import ComposableArchitecture
struct MyTodosView: View {
let store: StoreOf<MyTodos>
var body: some View {
WithViewStore(store) { viewStore in
VStack {
TextField(
"Add New Todo Item",
text: viewStore.binding(
get: \.newItemTitle,
send: { .newItemTitleChanged($0) }
)
)
Button("Add") { viewStore.send(.addNewItem) }
List(viewStore.items, id: \.self) {
Text($0)
}
}
.padding(20)
}
}
}
and finally we create?Store?for?MyTodos?feature as follows -
import SwiftUI
import ComposableArchitecture
@main
struct MyTestApp: App {
var body: some Scene {
WindowGroup {
MyTodosView(
store: Store(initialState: MyTodos.State()) {
MyTodos()
}
)
}
}
}
This was a very simple example of?ReducerProtocol?just to help you get started with it. In future, we will write additional tutorials to cover other aspects of?ReducerProtocol.
Using?ReducerProtocol?in TCA has some benefits, such as:
We will try to cover some of them next!
Thanks for reading!