Tableau 0.1.0

Tableau 0.1.0

Maintained by Aaron Bosnjak.



Tableau 0.1.0

  • By
  • Aaron Bosnjak

Tableau

Tableau is a library for making your table and collection view setup routine smaller, more declarative, more type safe, and simply more fun. It includes support for automatically diffing and animating data on your tables, ability for cells to emit custom events, and support for RxSwift.

A quick intro

With Tableau, you 'bind' information and functionality like data, cell types (or closures that provide cells), and event handlers to your table on a per-section basis (or just to the whole table if your table isn't sectioned).

For a sectioned table, we start by declaring an enum (or struct) that defines your sections and create a 'binder' object:

enum Section: TableViewSection {
    case first
    case second
    case third
    case fourth
}

let binder = SectionedTableViewBinder(tableView: self.tableView, sectionedBy: Section.self)

Then, we start 'binding chains' to one or multiple sections, kind of like in a switch statement. Here, we'll make three 'binding chains' to add different data and handlers to the sections we just declared:

var myModels: [MyModel] = ...

binder.onSection(.first)
    .bind(headerTitle: "FIRST SECTION")
    .bind(cellType: MyCustomTableViewCell.self, models: { myModels })
    .onDequeue { (row: Int, cell: MyCustomTableViewCell, model: MyModel) in
        // setup the dequeued 'cell' with the 'model'
    }
    .onTapped { (row, cell, model: MyModel) in
        // e.g. go to a detail view controller with the 'model'
    }

binder.onSections(.second, .third)
    .bind(headerTitles: [.second: "SECOND SECTION", .third: "THIRD SECTION"])
    .bind(cellProvider: { (tableView, section, row, model: MyOtherModel) -> UITableViewCell in 
        if ... {
            return tableView.dequeue(MyOtherTableViewCell.self)
        } else {
            return tableView.dequeue(MyCustomTableViewCell.self)
        }
    }, models: { () -> [MyOtherModel]
        ...
    })
    .dimensions(
        .cellHeight { section, row, model in 
            return UITableViewAutomaticDimension 
        },
        .estimatedCellHeight { _, _ in 120 })
    ...
    
binder.onAllOtherSections()
    .bind(cellType: MyOtherOtherTableViewCell.self, viewModels: { ... })
    ...

Cells are automatically dequeued for given 'model' arrays, and bound model and cell types are remembered and passed to other handlers on the binding chain. By having tables setup using chains like this, you can more clearly describe how your table views work in a way that reads more like the requirements you receive.

Custom cell events

Figuring out how to pass events like when a button is pressed or text is entered on a cell back up to your view controller is always a hassle, usually involving conformance to a bunch of different delegate protocols. To solve this, Tableau also gives you the ability for your cells to declare custom event enums that can be observed on your binding chains. This is done by conforming your cell to the ViewEventEmitting protocol and giving it a ViewEvent enum, like this:

class MyCustomTableViewCell: UITableViewCell, ViewEventEmitting {
    enum ViewEvent {
        case switchToggled(state: Bool)
        case buttonPressed
        case textEntered(text: String)
    }
    
    @objc func onSwitchToggled(switch: UISwitch) {
        self.emit(event: .switchToggled(state: switch.isOn)
    }
}

You can then observe for when cells emit events on a binding chain like this:

binder.onSection(.first)
    .bind(cellType: MyCustomTableViewCell.self, models: { myModels })
    .onEvent(from: MyCustomTableViewCell.self) { (row, cell, event, model: MyModel) in
        switch event {
        case .switchToggled(let state):
            // update something in the model
        case .buttonPressed:
            // perform some action
        case .textEntered(let text):
            // update something in the model
        }
    }

Diffing and animation

Tableau supports deep diffing and automatic animation of your table when its data changes. Without doing anything special to your models, Tableau can already determine when (and which) sections update and run a basic count on the changes to apply 'reload', 'insert', and 'delete' animations on sections.

However, if you want it, the library also gives you more powerful and more fine-grained control over diffing by taking into account conformance to Equatable and CollectionIdentifiable on the models you bind to provide more precise 'reload', 'insert', and 'delete' animations, as well as for tracking moves - all automatically!

Advanced features

You're a good developer, though - you look at these samples and think of all the limitations and edge cases you can run into with that (more than one cell or model type per section, custom header/footer view type, section data not known at compile time, infinite scroll...). Luckily, Tableau supports many more advanced features that cover cases like these and scales well with the complexity of your table, so you should never be so limited by the library that you find yourself resorting to the regular UIKit 'data source/delegate' routine.

Tableau is fully documented, including tutorials and working code samples (with documented walkthroughs!) in the 'TableauExample' Xcode project in the repository. Available tutorials to get you familiar with Tableau are as follows:

Installation

Tableau (will be) available through CocoaPods. To install it, simply add the following line to your Podfile:

pod 'Tableau'

Contributors

Aaron Bosnjak (email: [email protected], Twitter: @aaron_bosnjak)

Tableau is open to contributors! If you have a feature idea or a bug fix, feel free to open a pull request. Issues and feature ideas are tracked on this Trello board.

Acknowledgement

Tableau uses a fork of Tony Arnold's super-awesome Differ library to do its diffing work. If you like Tableau, make sure to give that repo a star as well!

License

Tableau is available under the MIT license, so do pretty much anything you want with it. As always, see the LICENSE file for more info.