ReusableDataSource for table and collection view written in Swift

SimpleDataSource

Simplifies data source implementation by reorganising responsibilities and using a data driven approach. Improves reusability and decreases the amount of boilerplate.

Usage

Note: For simplicity I'll be addressing UITableView only, but everything, including framework support, extends to UICollectionView

Responsibility reorganisation starts with moving the view model presentation to the cell.

struct MovieViewModel {
    let name: String
    let releaseYear: Int
}

class MovieTableViewCell: UITableViewCell {
    @IBOutlet weak var nameLabel: UILabel!
    @IBOutlet weak var releaseYearLabel: UILabel!
    ...
}

extension MovieTableViewCell: PresentingTableViewCell {
    func present(viewModel: MovieViewModel) {
        nameLabel.text = viewModel.name
        releaseYearLabel.text = String(viewModel.releaseYear)
    }
}

Next, specify the cell type that should be registered and dequeued for a particular view model. To be able to use the default implementations, TableViewCell.ViewModel must equal Self.

extension ActorViewModel: DequeuableTableViewCellViewModel {
    typealias TableViewCell = ActorTableViewCell
}

Instead of implementing a custom UITableViewDataSource, we will use SimpleTableViewDataSource. We simply initialise and set it as the tableViews dataSource.

class MovieViewController: UITableViewController {
    lazy var dataSource = SimpleTableViewDataSource()

    override func viewDidLoad() {
        super.viewDidLoad()

        tableView.dataSource = dataSource
    }
}

Finally, map and present the data. For the view model mapping we can use the tableViewPresentable computed property.

class MovieViewController: UITableViewController {
    ...
    let movies = [
        MovieViewModel(name: "Above the Law", releaseYear: 1988, actors: [
            ActorViewModel(name: "Steven Seagal"),
            ActorViewModel(name: "Pam Grier"),
            ActorViewModel(name: "Henry Silva")
            ]
        ),
        MovieViewModel(name: "Under Siege", releaseYear: 1992, actors: [
            ActorViewModel(name: "Steven Seagal"),
            ActorViewModel(name: "Gary Busey"),
            ActorViewModel(name: "Tommy Lee Jones")
            ]
        )
    ]
    
    func presentCellViewModels() {
        let cellViewModels = movies
            .map { movie -> [AnyDequeuableTableViewCellViewModel] in
                var movieViewModels = [movie.tableViewPresentable]

                movieViewModels.append(contentsOf: movie.actors.map { $0.tableViewPresentable })

                return movieViewModels
            }

        dataSource.present(viewModels: cellViewModels, onTableView: tableView)
    }

That's it! Check it out by running the demo project.

For a more detailed showcase, take a look at this blog post.

Installation

Carthage

github "Rep2/ReusableDataSource" ~> 0.3

Detailed overview

DequeuableTableViewCellViewModel protocol specifies how to register and dequeue a table view cell from a view model.

protocol DequeuableTableViewCellViewModel {
    associatedtype TableViewCell: PresentingTableViewCell

    func dequeueReusableCell(forRowAt indexPath: IndexPath, onTableView tableView: UITableView) -> TableViewCell
    func registerTableViewCell(onTableView tableView: UITableView)
}

PresentingCollectionViewCell protocol defines a view model type that a cell can present. It also specifies the cell source which is used during the cell registration.

protocol PresentingCollectionViewCell {
    associatedtype ViewModel: DequeuableCollectionViewCellViewModel

    static var source: CellSource { get }

    func present(viewModel: ViewModel)
}

extension PresentingCollectionViewCell {
    static var source: CellSource {
        return .class
    }
}

By combining the previously defined associated types, we can provide default implementations for cell registration and dequeue, as long as TableViewCell is UITableViewCell, and TableViewCell.ViewModel equals view model type that implemented the DequeuableTableViewCellViewModel protocol.

extension DequeuableTableViewCellViewModel where TableViewCell: UITableViewCell, TableViewCell.ViewModel == Self {
    func dequeueReusableCell(forRowAt indexPath: IndexPath, onTableView tableView: UITableView) -> TableViewCell {
        let cell = tableView.dequeueReusableCell(for: indexPath) as TableViewCell

        cell.present(viewModel: self)

        return cell
    }
}

extension DequeuableTableViewCellViewModel where TableViewCell: UITableViewCell {
    func registerTableViewCell(onTableView tableView: UITableView) {
        tableView.register(cell: TableViewCell.self, reusableCellSource: TableViewCell.source)
    }
}

As the DequeuableTableViewCellViewModel contains an associated type, we can only use it as a generic constraint. To be able to pass it as a parameter, we need to remove the associated type using type-erasure. This is the role of AnyDequeuableTableViewCellViewModel.

tableViewPresentable is a stored property of DequeuableTableViewCellViewModel that simplifes this transformation.

class AnyDequeuableCollectionViewCellViewModel {
    let dequeueAndPresentCell: (UICollectionView, IndexPath) -> UICollectionViewCell
    let registerCell: (UICollectionView) -> Void
}

extension DequeuableTableViewCellViewModel where TableViewCell: UITableViewCell {
    var tableViewPresentable: AnyDequeuableTableViewCellViewModel {
        return AnyDequeuableTableViewCellViewModel(
            dequeueAndPresentCell: { (tableView: UITableView, indexPath: IndexPath) -> UITableViewCell in
                return self.dequeueReusableCell(forRowAt: indexPath, onTableView: tableView)
            },
            registerCell: { (tableView: UITableView) in
                self.registerTableViewCell(onTableView: tableView)
            }
        )
    }
}

SimpleTableViewDataSource implements the UITableViewDataSource by using the register and dequeue closures.

class SimpleTableViewDataSource: NSObject {
    var viewModels = [[AnyDequeuableTableViewCellViewModel]]()

    var automaticallyRegisterReuseIdentifiers: Bool

    init(automaticallyRegisterReuseIdentifiers: Bool = true) {
        self.automaticallyRegisterReuseIdentifiers = automaticallyRegisterReuseIdentifiers

        super.init()
    }

    func present(viewModels: [[AnyDequeuableTableViewCellViewModel]], onTableView tableView: UITableView) {
        self.viewModels = viewModels

        if automaticallyRegisterReuseIdentifiers {
            viewModels
                .flatMap { $0 }
                .forEach { $0.registerCell(tableView) }
        }

        tableView.reloadData()
    }

    func present(viewModels: [AnyDequeuableTableViewCellViewModel], onTableView tableView: UITableView) {
        present(viewModels: [viewModels], onTableView: tableView)
    }
}

extension SimpleTableViewDataSource: UITableViewDataSource {
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        return viewModels[indexPath.section][indexPath.row].dequeueAndPresentCell(tableView, indexPath)
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return viewModels[section].count
    }

    func numberOfSections(in tableView: UITableView) -> Int {
        return viewModels.count
    }
}

By default SimpleTableViewDataSource registers a cell each time it's presented. This means that the number of cell registrations is the same as the number of cell presentations. To remove this behavior set the automaticallyRegisterReuseIdentifiers to false.

GitHub