ReducerProtocol in The Composable Architecture (TCA)
unknown

ReducerProtocol in The Composable Architecture (TCA)

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:

  • Use the conforming type as a natural namespace to house our state and action types, allowing them to lose their prefix.
  • Hold onto dependencies directly in the conforming type, without needing a dedicated environment type.
  • Define reducers as methods on a type, rather than closures at the file scope, which can help with compiler performance and autocomplete suggestions.
  • Compose reducers using property wrappers and operators, rather than using combine and pullback.

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 —

  • list of todo items (for the shake of simplicity, we represent a single todo item as a string), so it will be an array of?String?type.
  • the second piece of data we need to keep track of, is the text for the new todo item, so it will be a?String.

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 -

  • the first one will handle changes in text field for new todo item
  • the second one will handle button tap for adding new todo item

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:

  • It makes your state and action types more concise and readable by using the conforming type as a natural namespace, allowing them to lose their prefix.
  • It simplifies the dependencies of your reducers by allowing you to hold onto them directly in the conforming type, without needing a dedicated environment type.
  • It improves the compiler performance and autocomplete suggestions by defining reducers as methods on a type, rather than closures at the file scope.
  • It makes composing reducers easier and more expressive by using property wrappers and operators, rather than using combine and pullback.
  • It unlocks testability of your features by allowing you to write integration tests and end-to-end tests for your reducers.

We will try to cover some of them next!

Please follow us on?Twitter?and Medium?for more updates.

Thanks for reading!

#SwiftUI?#Swift.org?#evangelistapps?#TheComposableArchitecture #swift #iOS #coding #iOS16

要查看或添加评论,请登录

Evangelist Apps的更多文章

社区洞察

其他会员也浏览了