The first question I would ask myself in this situation is whether any state in the view controller has to change based on inputs into the cell. If so, then option 2 is the best choice (assuming RxSwift.) If you are using delegates/closures instead of Observables then option 3 is handy to reduce the number of delegates required.
From your description, it doesn't sound like the view controller's state needs to be updated when the user taps the button, so option 1 sounds like the best.
Here is how I would likely implement it using my CLE architecture... You can think of the connect
functions as view models.
extension ViewController {
// The view controller doesn't have much to do. Just fetch the array of
// likables and show them on the table view.
func connect(api: API) {
api.response(.fetchLikables)
.bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: LikableCell.self)) { _, item, cell in
cell.connect(api: api, item: item)
}
.disposed(by: disposeBag)
api.error
.map { $0.localizedDescription }
.bind(onNext: presentScene(animated: true) { message in
UIAlertController(title: "Error", message: message, preferredStyle: .alert).scene {
$0.connectOK()
}
})
.disposed(by: disposeBag)
}
}
extension LikableCell {
// The cell has the interesting behavior. If the thing is liked, then the
// `likeButton` is selected. When the button is tapped, update the state
// and send the network request. If the request fails, then reset the state.
func connect(api: API, item: LikableThing) {
enum Input {
case tap
case updateSucceeded
case updateFailed
}
cycle(
input: likeButton.rx.tap.map(to: Input.tap),
initialState: (current: item.isLiked, reset: item.isLiked),
reduce: { state, input in
switch input {
case .tap:
state.current.toggle() // toggle the like state right away.
case .updateSucceeded:
state.reset = state.current // if server success, update the reset
case .updateFailed:
state.current = state.reset // if server fail, update the current state.
}
},
reaction: { args in
args
.filter { $0.1 == .tap } // only make network request when user taps
.flatMapLatest { state, _ in
return api.successResponse(.setLikable(id: item.id, isLiked: !state.current))
.map { $0 ? Input.updateSucceeded : Input.updateFailed }
}
}
)
.map { $0.current }
.bind(to: likeButton.rx.isSelected)
.disposed(by: disposeBag)
}
}
struct LikableThing: Decodable, Identifiable {
let id: Identifier<Int, LikableThing>
let isLiked: Bool
}
extension Endpoint where Response == [LikableThing] {
static let fetchLikables: Endpoint = Endpoint(
request: apply(URLRequest(url: baseURL)) { request in
// configure request
},
decoder: JSONDecoder()
)
}
extension Endpoint where Response == Void {
static func setLikable(id: LikableThing.ID, isLiked: Bool) -> Endpoint {
let request = URLRequest(url: baseURL)
// configure request
return Endpoint(request: request)
}
}