Old Patterns Made New
Because Truly New Patterns Require Iteration
I've been working with, and thinking about table data sources even more lately, so I have some refinements on my earlier thoughts.
Using C style enums for table sections and row counts was a common practice in Objective-C. It helped keep complex code maintainable, and it helped reduce all sorts of common errors. Looking at swift patterns, it's very interesting to see that a lot of developers are using a very similar style with Swift code:
final class OldTableDataSource: NSObject {
}
private extension OldTableDataSource {
enum Section: Int {
case Header, Body, Footer
static let numberOfSections = 3
enum HeaderRows: Int {
case Row1, Row2
static let numberOfRows = 2
}
enum BodyRows: Int {
case row1, row2, row3, row4
static let numberOfRows = 4
}
enum FooterRows: Int {
case row1, row2, row3, row4
static let numberOfRows = 4
}
}
// Because I'm not exhaustively covering the cases, I'm cheating with a generic
func cellForRow<RowType>(row: RowType) -> UITableViewCell {
return UITableViewCell()
}
}
extension OldTableDataSource: UITableViewDataSource {
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return Section.numberOfSections
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let section = Section(rawValue: section) else {
fatalError()
}
switch section {
case .Header: return Section.HeaderRows.numberOfRows
case .Body: return Section.BodyRows.numberOfRows
case .Footer: return Section.FooterRows.numberOfRows
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
guard let section = Section(rawValue: indexPath.section) else {
fatalError()
}
let cell: UITableViewCell
switch section {
case .Header:
guard let row = Section.HeaderRows.init(rawValue: indexPath.row) else {
fatalError()
}
cell = cellForRow(row)
case .Body:
guard let row = Section.BodyRows.init(rawValue: indexPath.row) else {
fatalError()
}
cell = cellForRow(row)
case .Footer:
guard let row = Section.FooterRows.init(rawValue: indexPath.row) else {
fatalError()
}
cell = cellForRow(row)
}
return cell
}
}
It's a bit more exhaustive than most implementations that I've seen, most of them use arrays to back the individual sections rather than nested enumerations, but this covers a table where you want control over every cell. It's a bit wordy in places, but adding a section will work and it will produce compile time information if the enumerations have items added or removed.
The place that it falls down is if the page has multiple states. Hiding sections or rows begins to require functions to adjust the index path. Figuring out which cell and data to use for a particular row also is going to get very wordy very quickly.
More Swift-y
Starting to really take advantage of Swift enumerations means making real use of associated values, and embracing the ability to get more out of enums that just reliability. I'm still toying with the code quite a bit, but so in almost the same number of lines of code, I have something much closer to functional and with fewer points of potential failure:
final class newTableViewDataSource: NSObject {
private var tableSections: [Section]
init(header: Bool, footer: Bool) {
var sections: [Section] = []
if header {
sections.append(Section.Header(rows: [
Section.Row.HeaderLead(title: "Header"),
Section.Row.StandardCell(title: "Test", detail: nil),
]))
}
sections.append(Section.Body(rows: [
Section.Row.StandardCell(title: "First Row", detail: nil),
Section.Row.StandardCell(title: "Row", detail: "With Detail"),
Section.Row.StandardCell(title: "More", detail: nil),
Section.Row.StandardCell(title: "Detail?", detail:"Yes"),
Section.Row.StandardCell(title: "Test", detail: nil),
]))
if footer {
sections.append(Section.Footer(rows: [
Section.Row.StandardCell(title: "Almost Done", detail: "Really!"),
Section.Row.FooterFinish(title: "...Done"),
]))
}
self.tableSections = sections
}
}
private extension newTableViewDataSource {
enum Section {
case Header(rows: [Row])
case Body(rows: [Row])
case Footer(rows: [Row])
enum Row {
case HeaderLead(title: String)
case StandardCell(title: String, detail: String?)
case FooterFinish(title: String)
}
}
}
extension newTableViewDataSource.Section {
var rows: [Row] {
switch self {
case .Body(let rows): return rows
case .Header(let rows): return rows
case .Footer(let rows): return rows
}
}
}
extension newTableViewDataSource.Section.Row {
var title: String {
switch self {
case .FooterFinish(let title): return title
case .HeaderLead(let title): return title
case .StandardCell(let title, _): return title
}
}
var detail: String? {
switch self {
case .FooterFinish, .HeaderLead: return nil
case .StandardCell(_, let detail): return detail
}
}
}
extension newTableViewDataSource: UITableViewDataSource {
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return tableSections.count
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableSections[section].rows.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let row = tableSections[indexPath.section].rows[indexPath.row]
let cell: UITableViewCell
switch row {
case .FooterFinish:
cell = tableView.dequeueReusableCellWithIdentifier("footerIdentifer", forIndexPath: indexPath)
case .HeaderLead:
cell = tableView.dequeueReusableCellWithIdentifier("headerIdentifier", forIndexPath: indexPath)
case .StandardCell:
cell = tableView.dequeueReusableCellWithIdentifier("standardIdentifier", forIndexPath: indexPath)
}
cell.textLabel?.text = row.title
cell.detailTextLabel?.text = row.detail
return cell
}
}
This doesn't require statically assigning the number of rows per section, and the init
code supplied can generate 4 different table structures off of those two boolean values, with zero index math.
The extensions onto the Section
and Row
enumerations add clean subscript access two layers deep, and the ability to share common cell configuration code for all of the enum values.
I'm contemplating taking a third pass that uses structs to store the enum value, and other associated values rather than using enums with associated values at all, but I think that may look conceptually less clean, though with the plus side of not having to write the enum extensions for every type.