Skip to content

Instantly share code, notes, and snippets.

@peteranny
Last active March 6, 2024 05:37
Show Gist options
  • Save peteranny/76015eba6da99a48147e70689e3c3b9d to your computer and use it in GitHub Desktop.
Save peteranny/76015eba6da99a48147e70689e3c3b9d to your computer and use it in GitHub Desktop.
Combine-version twin of the event-driven binding to the data source of a table view. Referenced by: https://medium.com/@tingyishih/mvvm-data-binding-with-rxswift-single-interface-practice-1a7f5f1a655d
import Combine
import CombineCocoa // To allow publisher extensions such as button.tapPublisher
import CombineDataSources // To allow the event-driven data source of the table view
import CombineExt // To enable the Combine extension Publishers.withLatestFrom. Ref: https://github.com/CombineCommunity/CombineExt
import UIKit
class SimpleTableViewController: UITableViewController {
private let viewModel = SimpleTableViewModel()
private var cancellables: [AnyCancellable] = []
override func viewDidLoad() {
super.viewDidLoad()
// Create table data source
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
let itemsController = TableViewItemsController<[[Int]]> { controller, tableView, indexPath, item in
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = "Item \(item)"
return cell
}
tableView.delegate = nil
tableView.dataSource = nil
let itemsSubject = ReplaySubject<[Int], Never>(bufferSize: 1)
itemsSubject.subscribe(tableView.rowsSubscriber(itemsController))
let didSelectItemPublisher = tableView.didSelectRowPublisher
.withLatestFrom(itemsSubject, resultSelector: { indexPath, items in items[indexPath.row] })
// Bind the input to the view model
let input = SimpleTableViewModel.Input(
fetchItems: Just(()).eraseToAnyPublisher(),
selectItem: didSelectItemPublisher.eraseToAnyPublisher()
)
let output = viewModel.bind(input)
// Bind the output from the view model
output.items
.sink(receiveValue: { itemsSubject.send($0) })
.store(in: &cancellables)
output.showItemContent
.map { content in UIAlertController(content: content) }
.sink(receiveValue: { [weak self] alert in self?.present(alert, animated: true) })
.store(in: &cancellables)
cancellables.append(contentsOf: output.cancellables)
}
}
extension UIAlertController {
convenience init(content: String) {
self.init(title: content, message: nil, preferredStyle: .alert)
addAction(UIAlertAction(title: "OK", style: .default))
}
}
import Combine
protocol ViewModelBinding {
associatedtype Inputs
associatedtype Outputs
func bind(_ inputs: Inputs) -> Outputs
}
class SimpleTableViewModel: ViewModelBinding {
struct Input {
let fetchItems: AnyPublisher<Void, Never>
let selectItem: AnyPublisher<Int, Never>
}
struct Output {
let items: AnyPublisher<[Int], Never>
let showItemContent: AnyPublisher<String, Never>
let cancellables: [AnyCancellable]
}
func bind(_ input: Input) -> Output {
let itemsSubject = CurrentValueSubject<[Int], Never>([])
let bindFetchItems = input.fetchItems
.flatMap { [itemService] in itemService.fetchItems() }
.sink(receiveCompletion: { _ in }, receiveValue: { itemsSubject.send($0) })
let showItemContentSubject = PassthroughSubject<String, Never>()
let bindSelectItem = input.selectItem
.flatMap { [itemService] item in itemService.fetchItemContent(item) }
.sink(receiveCompletion: { _ in }, receiveValue: { showItemContentSubject.send($0) })
// Form the output event streams for the binder to subscribe
return Output(
items: itemsSubject.eraseToAnyPublisher(),
showItemContent: showItemContentSubject.eraseToAnyPublisher(),
cancellables: [bindFetchItems, bindSelectItem]
)
}
private let itemService = ItemService()
}
class ItemService {
func fetchItems() -> Future<[Int], Never> {
Future { promise in promise(.success(Array(0...10))) }
}
func fetchItemContent(_ item: Int) -> Future<String, Never> {
Future { promise in promise(.success("Content of \(item)")) }
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment