Last active
March 6, 2024 05:37
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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