We cannot rely on a function of the index to determine the cell type for a given index and we cannot rely on the builtin section support, because we have to implement section Headers, Subheaders, Subfooters and Footers.
When things get this complex, a different kind of solution is necessary.
One way to think about the task is this: transform the input data stream to an output stream and then print it on paper, or at least on a web page, of whatever length is required. The trick is to not commit to the actual output styling - its just an abstraction. Once the output data stream has been created, it just has to be collected in such a way that the tableView can easily display the output.
This technique is demonstrated in a demo app that you can find at ReportTableAdapterDemo.
In order to satisfy the requirements for the demo, we can break down the app into a number of tasks
First, we have to organize the data for the demo.
In the demonstration code each transaction is represented like this:
struct TransactionModel {
let group: String
let date: String;
let description: String
let amount: String
let debit: String
}
The TransactionModel
represents the data model that a service layer would deliver to the viewController. The data is not delivered exactly as it should be - we will revisit this in the next post. It does not matter that the data is coming from two different sources. What matters is that the data can be coerced to the same format, which allows the processing for the two sources to be identical - think: protocols.
The data represents the two input streams: authorizedData
and postedData
. Both streams are simply arrays of TransactionModel
s. Each transaction looks similar to this:
TransactionModel( group: "P",
date: "2016-05-01", description: "The Rex", amount: "3.11", debit: "D" )
You can view the whole input stream here.
Next, we want to design the rows we are going to “print”. First we visually design with IB and then think about the code to generate the lines.
We specify a cell to represent each type of row that can be displayed. There are 7 cell types, corresponding to:
Here is a snapshot of the cell layouts in IB:
       
The purpose of each cell type is should be obvious with respect to the screen shots above.
Notice 2 subtleties: the footer and the grandfooter look similar; and each Date group is displayed as a block in one colour, where the top and bottom margins are equal
Part of the solution strategy is to avoid computation wherever it is possible to use structures instead. Even though the footer and grandfooter rows look similar, we use a separate cell type for each because they behave differently in the following ways:
By separating out the behaviour by type, it will be easy to change either of the two row styles, without affecting the other, when a design change is required.
In the same way, the detail cell could have been designed so that it would change its height dynamically, depending on whether it was the last row in a group. We would have to introduce a boolean to explicitly record the actual type that the cell should be. Instead of this, a subfooter cell is introduced just to take up the space below the last detail. It is highlighted in the screen shot above.
As mentioned earlier, the strategy is to transform the input stream into an output stream. The output stream can then be displayed by the tableview.
Since each section is identical in format, we only have to write a transformer for one input data stream. We can reuse the transformer for the second data stream.
The TransactionListTransformerOutput
protocol supports the notion that we could be sending the output to anywhere. It is shown here:
protocol TransactionListTransformerOutput {
func appendHeader(title: String)
func appendSubheader(date: String )
func appendDetail(description: String, amount: String)
func appendSubfooter()
func appendFooter(total: String)
func appendGrandFooter(grandTotal: String)
func appendMessage(message: String)
}
Each method in the protocol represents a row type of the output. The protocol’s name derives from the fact that it represents the output of the transformation of the TransactionList.
The transformer, called from viewDidLoad
is a follows:
private func transformFromTwoSources() {
appendSection(transactions: authorizedData, title: "Authorized")
appendSection(transactions: postedData, title: "Posted")
adapter.appendGrandFooter( grandTotal: String(grandTotal) )
}
appendSection
is implemented as follows:
private func appendSection(transactions: [TransactionModel]?, title: String) {
adapter.appendHeader( title: "\(title) Transactions" )
if let transactions = transactions {
var i = 0
var transaction = next(transactions: transactions, i: &i )
var total = 0.0
while transaction != nil {
let curDate = transaction!.date
adapter.appendSubheader(date: curDate)
while transaction != nil && transaction!.date == curDate {
var amount: String
if (transaction!.debit != "D") {
amount = "-" + transaction!.amount
}
else {
amount = transaction!.amount
}
total += Double(amount)!
adapter.appendDetail(description: transaction!.description, amount: amount )
transaction = next(transactions: transactions, i: &i )
}
adapter.appendSubfooter()
}
adapter.appendFooter(total: String(total) )
grandTotal += total
}
else {
adapter.appendMessage(message: "\(title) Transactions are not currently available. You might want to call us and tell us what you think of that!")
}
}
appendSection
transforms the input TransactionModel
s to an output stream defined by the TransactionListTransformerOutput
protocol. Given the requirement that the input streams come from two different sources, a TransactionModel
could also be implemented as an abstract protocol, representing two different concrete models.
The transformer assumes that the transactions are sorted by date. The code is organized so that
nil
The first point is implemented by this idiom:
private func next(transactions: [TransactionModel], i: inout Int ) ->TransactionModel? {
let transaction = ( i < transactions.count ) ? transactions[ i ] : nil
i += 1
return transaction
}
At the beginning of each date iteration, the current date is set from the transaction date of the first record in the group. The rest of the code in this block represents the current date group. All rows for this group, including the Subheader, Details and Subfooter, will be the same band colour. If we were required to print the subtotal for the date, we could easily set-up a date total in this block; setting it to zero at the top of the loop.
In the inner iteration the data is transformed and added to the section total. As soon this processing is complete, the next transaction is retrieved.
Data is sent to the output stream as it is produced.
By the way, the structure for the transformer was derived systematically from the output requirement using a technique called Data directed Design, which will be discussed in a future article.
From the break down of the tasks listed above, it seems like we need (at least) two major classes: one to transform the input data stream and one to supply the output of the transformation to the tableView as a dataSource.
There is a lot of talk these days about the problems associated with the Massive ViewController (MVC). The problem with MVCs is that they are hard to understand because of large scope and they are hard to change due to coupling.
One simple-to-implement best practice, which will allow us to avoid MVCs that contain a tableView is to create a separate class to act as the datasource for a tableView. Almost every demonstration, by Apple or otherwise, implements the dataSource as part of the viewController. Once you get used to separating them, you will see that implementing a dataSource in a ViewController is an anti-pattern leading to code bloat. You can read more about this in Lighter View Controllers.
By moving the the task of collecting the TransformerOutput to another class, called the Adapter, we neatly split the application into two major components. The viewController generates the rows and the adapter collects the rows.
Both the ViewController and the Adapter have other responsibilities. The ViewController is also responsible for managing its views and the adapter is responsible for producing cells for the table in its role as a TableViewDataSource. That being said, separating out the Adapter from the ViewController is a good start to separating responsibilities. The resolution of the other conflicting responsibilities is left to a future article.
The adapter implements three protocols: UITableViewDataSource
, UITableViewDelegate
and TransactionListTransformerOutput
. Each protocol has been implemented as an extension. This is a great practice because it reduces the scope of the protocol implementations and allows the compiler to accurately place messages about missing implementations .
The only thing that the extensions do not implement is the data and this is left to the class.
class TransactionListAdapter: NSObject {
fileprivate var rowList = [Row]()
fileprivate var odd = false
}
Although the TransactionListTransformerOutput
protocol describes the output of the Transformer, it also describes the input to the Adapter. The naming scheme suggests that the rows will be appended to the output. The response to each message of the protocol is to append a row to the rowList
.
extension TransactionListAdapter: TransactionListTransformerOutput {
func appendHeader(title: String ) {
rowList.append( HeaderRow( title: title ) )
}
private static let inboundDateFormat = DateFormatter.dateFormatter( format: "yyyy'-'MM'-'dd" )
private static let outboundDateFormat =
DateFormatter.dateFormatter( format: "MMM' 'dd', 'yyyy" )
func appendSubheader(date inboundDate: String) {
odd = !odd
let date = TransactionListAdapter.inboundDateFormat.date( from: inboundDate)!
let dateString = TransactionListAdapter.outboundDateFormat
.string(from: date ).uppercased()
rowList.append( SubheaderRow( title: dateString, odd: odd ) )
}
func appendDetail( description: String, amount: String ) {
rowList.append( DetailRow( description: description, amount: amount, odd: odd ) )
}
func appendSubfooter() {
rowList.append(SubfooterRow( odd: odd ))
}
func appendFooter(total: String) {
odd = !odd
rowList.append(FooterRow(total: total, odd: odd))
}
func appendGrandFooter(grandTotal: String) {
rowList.append(GrandFooterRow(grandTotal: grandTotal))
}
func appendMessage(message: String) {
rowList.append(MessageRow(message: message))
}
}
The append
methods are converting the data into Row
s. the data in each row is in a format that will be used directly for display. By the time all of the rows are generated, there are no decisions left to be made with respect to height, colour, view configuration and data display content. The data retained in the Rows will be assigned directly to the Controls in the Cells.
The band colour has been captured by the odd property of the row. The actual colour code could have been stored in each row, but colour specification is a best confined to the view, so odd
is used as a proxy.
The Rows are implemented as structs
. Less code is required to implement the Rows using structs than classes, mostly because constructors do not have to be specified. Using let
, they are also immutable.
In the future, for testing, we will want to compare the rows and Swift directly supports equality comparison of structs (of scalars).
Each row type implements the Row
protocol.
private protocol Row {
var cellId: CellId { get }
var height: CGFloat { get }
}
The cellId
property and associated enum are used to generate the identifier used by dequeueReusableCell(withIdentifier:)
. The height
property is the height of the cell to be returned by tableView(_:heightForRowAt:)
.
private enum CellId: String {
case header
case subheader
case detail
case subfooter
case footer
case grandfooter
case message
}
The row types are implemented as follows:
private struct HeaderRow: Row {
let title: String
let cellId: CellId = .header
let height: CGFloat = 60.0
}
private struct SubheaderRow: Row {
let title: String
let odd: Bool
let cellId: CellId = .subheader
let height: CGFloat = 34.0
}
private struct DetailRow: Row {
let description: String
let amount: String
let odd: Bool
let cellId: CellId = .detail
let height: CGFloat = 18.0
}
private struct SubfooterRow: Row {
let odd: Bool
let cellId: CellId = .subfooter
let height: CGFloat = 18.0
}
private struct FooterRow: Row {
let total: String
let odd: Bool
let cellId: CellId = .footer
let height: CGFloat = 44.0
}
private struct GrandFooterRow: Row {
let grandTotal: String
let cellId: CellId = .grandfooter
let height: CGFloat = 60.0
}
private struct MessageRow: Row {
let message: String
let cellId: CellId = .message
let height: CGFloat = 100.0
}
The Rows are pretty simple. They contain precisely the data needed for display; no further conversions are necessary. The result is that the Rows are pure immutable ViewModels, having no behaviour.
Where to perform data conversion is interesting because there are many choices of where to place it: in the cell binding function, in the initializer for the ViewModel, in the transformer output function (the appender), or in the transformer itself. For now, most of the conversions have been placed in either the transformer or in the transformer output, but I think there is a better placement strategy. We will discuss this choice again in the next post.
As discussed, the second major responsibility of cellForRowAt
is to assign values to the views. The TransactionCell
protocol specifies the interface to bind the data in a Row to a Cell for display. Each Cell must implement bind(row:)
.
The Row banding colour is a behaviour that is implemented by many of the cells. Since there are only two band colours, a boolean property called odd will be used to capture the banding colour of the cell with respect to the group position. This behaviour is supplied by setBackgroundColour(odd:)
.
private protocol TransactionCell {
func bind(row: Row)
}
private extension TransactionCell where Self: UITableViewCell {
func setBackgroundColour(odd: Bool) {
let backgroundRgb = odd ? 0xF7F8FC : 0xDDDDDD
backgroundColor = UIColor( rgb: backgroundRgb )
}
}
Protocol Extensions are a great place to put reusable methods that usually end up in a base class.
It would have been better to place the cell classes in the scope of the TransactionListAdapter
or at least to make them private to the file, but Interface Builder does not seem to be able to find them in either of these situations, so we will use private IBOutlets.
There is not much left to do in the implementation of the bind
methods - just cast, then set a few controls and the background colour.
class HeaderCell: UITableViewCell, TransactionCell {
@IBOutlet private var titleLabel: UILabel!
fileprivate func bind(row: Row) {
let headerRow = row as! HeaderRow
titleLabel.text = headerRow.title
}
}
class SubheaderCell: UITableViewCell, TransactionCell {
@IBOutlet private var titleLabel: UILabel!
fileprivate func bind(row: Row) {
let subheaderRow = row as! SubheaderRow
titleLabel.text = subheaderRow.title
setBackgroundColour(odd: subheaderRow.odd)
}
}
class DetailCell: UITableViewCell, TransactionCell {
@IBOutlet private var descriptionLabel: UILabel!
@IBOutlet private var amountLabel: UILabel!
fileprivate func bind(row: Row) {
let detailRow = row as! DetailRow
descriptionLabel.text = detailRow.description
amountLabel.text = detailRow.amount
setBackgroundColour(odd: detailRow.odd)
}
}
class SubfooterCell: UITableViewCell, TransactionCell {
fileprivate func bind(row: Row) {
let subFooterRow = row as! SubfooterRow
setBackgroundColour(odd: subFooterRow.odd)
}
}
class FooterCell: UITableViewCell, TransactionCell {
@IBOutlet private var totalLabel: UILabel!
fileprivate func bind(row: Row) {
let footerRow = row as! FooterRow
totalLabel.text = footerRow.total
setBackgroundColour(odd: footerRow.odd)
}
}
class GrandFooterCell: UITableViewCell, TransactionCell {
@IBOutlet private var totalLabel: UILabel!
fileprivate func bind(row: Row) {
let grandFooterRow = row as! GrandFooterRow
totalLabel.text = grandFooterRow.grandTotal
}
}
class MessageCell: UITableViewCell, TransactionCell {
@IBOutlet private var messageLabel: UILabel!
fileprivate func bind(row: Row) {
let messageRow = row as! MessageRow
messageLabel.text = messageRow.message
setBackgroundColour(odd: true)
}
}
At last we can move on to the implementation of the UITableViewDataSource
and UITableViewDelegate
.
extension TransactionListAdapter: UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let row = rowList[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: row.cellId.rawValue, for: indexPath)
(cell as! TransactionCell).bind(row: row )
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return rowList.count
}
}
extension TransactionListAdapter: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return rowList[ indexPath.row ].height
}
}
As you can see, tableView(_:numberOfRowsInSection:)
is trivial.
tableView(_:cellForRowAt:)
is more interesting in that it is also fairly simple: just access the row, dequeue the cell for the row’s type (cellId), bind the row data to the cell and return the cell. How simple is that.
tableView(_:heightForRowAt:)
simply returns the height given by the row.
It is interesting how trivial the implementation of tableView(_:cellForRowAt:)
is. All of the usual work of deciding which cell is being setup is being handled automatically via polymorphism - no switch statements required. All of the usual control assignments are relegated to the Cell classes that own the controls.
The UITableViewDataSource
knows only about TransactionCells
. Each TransactionCell knows what kind of Row
it can handle.
This solution, which involves generating rows to be consumed by a TableView adapter can be used whenever the data display requirement is non-trivial. I recommend using this kind of solution even when there is a one to one mapping of cells to input steam data. The if
statements in a typical cellForRowAtIndex
implementation are usually related to determination of type and the easiest way to simplify the code is to use structures to capture ViewModels which can be displayed directly by the Cells. This greatly simplifies the code by distributing the various concerns to smaller, more specific, classes.
We should avoid performing calculations to dynamically determine types, because calculations produce bugs. Structures are much easier to understand and their use does not provide an opportunity to produce bugs.
Imagine if all 3 protocols were implemented in the ViewController. All 325 lines of code would have resided in one file. It is easier to test that the Transformation is working correctly when classes are more specific in their responsibilities.
The Cells and Rows could have been moved into files of their own, except that the Rows would have to be internal scope, not private, because they are used by the Cells. It’s not that the internal scoping is an issue, it’s that the Row names are too general for the increased scope. Sometimes it’s just better to use better names to make things safe than to use private access. We will discuss this further, in the next blog article.
It is extremely easy to extend this pattern further with new cell and row types. I didn’t think twice about how to add the extra space in the banded block.
By the way, in Android, ListViews and RecyclerViews do not have builtin support for sections, so I use the kind of solution presented here even when the requirement is only for single repeating sections. In iOS, I never use UITableView section support unless I need to display floating headers - I find it easier to use the kind of solution presented here.
In the next instalment, we will look at how enums can be used to replace structs to simplify the code even further.