Bind UITableView with Combine DataSource

Viewed 3205

I want to directly link a UITableView with a @Published attribute without using DiffableDataSouce.

If I make the person

struct Person {
    let name: String
}

and create the data array:

@Published
var people = [Person(name: "Kim"), Person(name: "Charles")]

So I want to bind my UITableView directly, with something like:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return $people.count()
}

But this gives the error

Cannot convert return expression of type 'Publishers.Count<Published[Person]>.Publisher>' to return type 'Int'

1 Answers

The problem here is that the UITableViewDataSource is pull based (the framework pulls data from your code) but Publishers are push based (they push data to something.) That means that in order to make it work, you need a Mediator (a la the Mediator pattern.)

One option would be to bring in RxSwift/RxCocoa and the RxCombine project to translate between Combine and RxSwift and use the functionality where this already exists. That's a lot for this one ask, but maybe you have other areas where RxCocoa could streamline your code as well.

For just this ask, here is a Mediator that I think would work:

@available(iOS 13.0, *)
final class ViewController: UIViewController {

    var tableView: UITableView = UITableView()
    @Published var people = [Person(name: "Kim"), Person(name: "Charles")]
    var cancellable: AnyCancellable?

    override func viewDidLoad() {
        super.viewDidLoad()

        tableView.frame = view.bounds
        tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
        view.addSubview(tableView)

        cancellable = $people.sink(receiveValue: tableView.items { tableView, indexPath, item in
            let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
            cell.textLabel?.text = item.name
            return cell
        })

        DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
            self.people = [Person(name: "Mark"), Person(name: "Allison"), Person(name: "Harold")]
        }
    }
}

extension UITableView {
    func items<Element>(_ builder: @escaping (UITableView, IndexPath, Element) -> UITableViewCell) -> ([Element]) -> Void {
        let dataSource = CombineTableViewDataSource(builder: builder)
        return { items in
            dataSource.pushElements(items, to: self)
        }
    }
}

class CombineTableViewDataSource<Element>: NSObject, UITableViewDataSource {

    let build: (UITableView, IndexPath, Element) -> UITableViewCell
    var elements: [Element] = []

    init(builder: @escaping (UITableView, IndexPath, Element) -> UITableViewCell) {
        build = builder
        super.init()
    }

    func pushElements(_ elements: [Element], to tableView: UITableView) {
        tableView.dataSource = self
        self.elements = elements
        tableView.reloadData()
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        elements.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        build(tableView, indexPath, elements[indexPath.row])
    }
}
Related