I'm following the To Dos exercise in the tour of the the Composable Architecture. As an exercise, I'm modifying the the end of episode two to implement filtering the To Dos by a case insensitive contains if search term state exists.
I'm having trouble scoping the Store<AppState, AppAction>
to a Store<SearchTerm?, SearchTermAction>
as per the please-don't-judge-me-I'm-learning code below. Where I have written the comment // COMPILER ERROR HERE
, the compiler tells me:
Cannot convert value of type 'Store<SearchTerm?, AppAction>' to expected argument type 'Store<SearchTerm?, SearchTermAction>'
Just like the TodoView
, I would expect I can convert between the global AppAction
and local SearchTermAction
. Can anyone please suggest where I am going wrong? Thanks!
If I can overcome this hurdle, my next step is to pull back the search term reducer into the app reducer. I don't know if pull back is the right tool for composing the app reducer with the search term reducer.
import SwiftUI
import ComposableArchitecture
struct AppState: Equatable {
var allTodos = [Todo]()
var todos = [Todo]()
var searchTerm: SearchTerm?
}
struct SearchTerm: Equatable {
var filter: String
}
enum AppAction {
case todo(index: Int, action: TodoAction)
case searchTermChanged(SearchTerm?)
}
struct AppEnvironment {
}
struct Todo: Equatable, Identifiable {
var description = ""
var isComplete = false
let id: UUID
}
enum TodoAction {
case checkboxTapped
case textFieldChanged(String)
}
struct TodoEnvironment {
}
let todoReducer = Reducer<Todo, TodoAction, TodoEnvironment>() { state, action, environment -> Effect<TodoAction, Never> in
switch action {
case .checkboxTapped:
state.isComplete.toggle()
return .none
case .textFieldChanged(let text):
state.description = text
return .none
}
}
//let listReducer = Reducer<AppState, AppAction, AppEnvironment>() { state, action, environment -> Effect<AppAction, Never> in
// switch action {
// case .todo:
// break
// case .searchTermChanged(let term):
// state.searchTerm = term
//
// if let term = term {
// state.todos = state.allTodos.filter { $0.description.localizedCaseInsensitiveContains(term.filter) }
// } else {
// state.todos = state.allTodos
// }
// }
// return .none
//}
enum SearchTermAction {
case filterTextChanged(String)
}
struct SearchTermEnvironment {
}
let searchTermReducer = Reducer<SearchTerm?, SearchTermAction, SearchTermEnvironment>() { state, action, environment -> Effect<SearchTermAction, Never> in
switch action {
case .filterTextChanged(let text):
if text.isEmpty {
state = nil
}
else {
var term = state ?? SearchTerm(filter: "")
term.filter = text
state = term
}
}
return .none
}
// Business logic defined as: how state is mutated and the effects of those mutations.
// Bind together: app state, actions and effects
// Responsibilites: perform mutations and return effects
let appReducer: Reducer<AppState, AppAction, AppEnvironment> = todoReducer
.forEach(state: \AppState.todos,
action: /AppAction.todo(index:action:),
environment: { _ -> TodoEnvironment in TodoEnvironment() })
// .combined(with: searchTermReducer.pullback(state: \AppState.searchTerm,
// action: /AppAction.searchTermChanged,
// environment: { _ in SearchTermEnvironment() }))
struct ContentView: View {
let store: Store<AppState, AppAction>
var body: some View {
WithViewStore(store) { viewStore in
VStack {
SearchTermView(store: store.scope(state: \.searchTerm, // transform AppState to Optional<SearchTerm> state
action: AppAction.searchTermChanged)) // COMPILER ERROR HERE. transform Optional<SearchTerm> to AppState
NavigationView {
WithViewStore(self.store) { viewStore in
List {
ForEachStore(self.store.scope(state: \.todos, action: AppAction.todo(index:action:)), content: TodoView.init(store:))
}
}
.navigationBarTitle("Todos")
}
}
}
}
}
struct SearchTermView: View {
let store: Store<SearchTerm?, SearchTermAction>
var body: some View {
WithViewStore(store) { viewStore in
TextField("Title", text: viewStore.binding(get: { $0?.filter ?? "" }, send: SearchTermAction.filterTextChanged))
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
}
struct TodoView: View {
let store: Store<Todo, TodoAction>
var body: some View {
WithViewStore(store) { todoViewStore in
HStack {
Button(action: { todoViewStore.send(.checkboxTapped) }) {
Image(systemName: todoViewStore.isComplete ? "checkmark.square" : "square")
}
.buttonStyle(PlainButtonStyle())
TextField("Untitled todo", text: todoViewStore.binding(get: \.description, send: TodoAction.textFieldChanged))
}
.foregroundColor(todoViewStore.isComplete ? .gray : nil)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(store: Store(initialState: AppState(allTodos: [
Todo(description: "Milk", isComplete: false, id: UUID()),
Todo(description: "Hand Soap", isComplete: true, id: UUID()),
Todo(description: "Eggs", isComplete: false, id: UUID())
]), reducer: appReducer, environment: AppEnvironment()))
}
}