I'm playing with the Composable Architecture, and I want to include it in a UIKit application, not using SwiftUI. I am having trouble wrapping my head around scoping a store of an array of items to a store of a single item.
My model, actions and reducers:
import ComposableArchitecture
import Foundation
// MARK: Single Todo
struct Todo: Equatable {
var id: Int
var text: String
}
enum TodoAction: Equatable {
case updateTodo(text: String)
}
struct TodoEnvironment {}
let todoReducer = Reducer<Todo, TodoAction, TodoEnvironment> { todo, action, _ in
switch action {
case .updateTodo(let text):
todo.text = text
return .none
}
}
// MARK: AppState (all todos)
struct AppState: Equatable {
var todos: [Todo] = []
}
enum AppAction: Equatable {
case setTodos([Todo])
case todo(id: Int, action: TodoAction)
}
struct AppEnvironment {
}
let appReducer = Reducer<AppState, AppAction, AppEnvironment>.combine(
Reducer { state, action, environment in
switch action {
case .setTodos(let todos):
state.todos = todos
return .none
case .todo:
return .none
}
},
todoReducer.forEach(
state: \.todos,
action: /AppAction.todo(id:action:),
environment: { _ in TodoEnvironment() }
)
)
And a simple ViewController. Don't mind the forced unwrapped optionals, just a test project and yes, using a storyboard.
import UIKit
import Combine
import ComposableArchitecture
class ViewController: UIViewController {
@IBOutlet private var tableView: UITableView!
private var subscriptions = Set<AnyCancellable>()
private var store: Store<AppState, AppAction>!
private var viewStore: ViewStore<AppState, AppAction>!
override func viewDidLoad() {
store = Store(initialState: AppState(), reducer: appReducer, environment: AppEnvironment())
viewStore = ViewStore<AppState, AppAction>(store)
super.viewDidLoad()
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Load", style: .done, target: self, action: #selector(load))
viewStore.publisher.todos.sink { todos in
print(todos)
self.tableView.reloadData()
}.store(in: &subscriptions)
}
@objc func load() {
viewStore.send(.setTodos([Todo(id: 1, text: "Buy milk")]))
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
viewStore.todos.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let todo = viewStore.todos[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = todo.text
return cell
}
}
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let todo = viewStore.todos[indexPath.row]
// Create a scoped store for a single todo
let scopedStore = store.scope(state: { $0.todos }, action: AppAction.todo)
// scopedStore is now of type Store<[Todo], (Int, TodoAction)>, which is a problem
let scopedViewStore = ViewStore<Todo, TodoAction>(scopedStore)
// Cannot convert value of type 'Store<[Todo], (Int, TodoAction)>' to expected argument type 'Store<Todo, TodoAction>'
}
}
In the didSelectRowAt
method I'd want to navigate to another screen, showing (and operating on) a single Todo item. So I'd want to pass it a Store<Todo, TodoAction>
, and it can create its own ViewStore
. But creating a scoped Store
returns Store<[Todo], (Int, TodoAction)>
, which is not the correct signature to create a ViewStore<Todo, TodoAction>
with.
This is something that ForEachStore
handles in SwiftUI, but how I am supposed to handle this in a simple UIKit app? How do I create a store that operates on a single Todo
item, and how do I then actually give it that single Todo
?