In this post, I present a technique I use a lot when building a UITableViewController in Swift, but which I haven’t seen used by many other people very often.
Background
This technique was inspired by this years’s edition of the legendary Stanford CS193p on iTunes U (presented by the just as legendary Paul Hegarty) : iTunes U – Developing iOS 8 apps with Swift, and by this M2M site which for years now has specialized on giving unofficial solutions to the assignments (I suppose that Stanford students have a chance of having their work corrected, but there is no « official » solution to the assignments for the iTunes U followers). In this course, the 4th assignment dealt specifically with table views. Namely, one of the requirements was:
While you might be tempted to deal with this with large if-then or switch statements in your UITableViewDataSource and navigation methods, a cleaner approach would be to create an internal data structure for your UITableViewController which encapsulates the data (both the similarities and differences) in the sections. For example, it’d be nice if numberOfSectionsInTableView, numberOfRowsInSection , and titleForHeaderInSection were all “one-liners”.
Context
Indeed, this requirement reminded me of an app that I have been developing for years and improving a bit at every new iOS release: ZEN Portfolio. Basically, this is a stocks manager that I wanted to be both very simple and very efficient. One of the most important features is that it gives you in your currency the gain or loss for a given share, taking into account both the stock quote and the currency rate if the stock is quoted in a currency different from yours.

The interesting part is here: for a given stock, I wanted to detail the calculation process. So when you tap on a cell of the stock list, you get pushed to another Table view controller, which looks like this:

This table view contains the following sections:
- General (number of shares and purchase date)
- Share price (purchase / current price)
- Stock valuation in the stock currency (purchase cost / current stock value)
- Currency rate (purchase / current rate) *
- Stock valuation in the user’s currency (purchase cost / current stock value)
- Finally, the gain or loss for the stock (value / %)
* should appear only if the stock currency is different from the user’s currency.
What does that little footnote means ? Well, let’s suppose you are a US citizen and wish to follow an AAPL stock. There is no currency conversion here, so in that case, there would only be 5 sections (section 4 for currency would not appear here). But if you are, say, a French citizen who happens to buy AAPL stock, then your gain or loss is maybe more dependent on the currency rate than on the stock variation itself. In that case, you would see all 6 sections.
In Objective-C
This is what my Table view data source methods would look like up to iOS 7:
number Of Sections:
1 2 3 4 5 6 7 8 9 |
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { // Return the number of sections. if (![self.item.currency isEqualToString:[[ZENGlobalSettings sharedStore] portfolioCurrency]]) { return 6; } else { return 5; } } |
number Of Rows In Section:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { // Return the number of rows in the section. int numberOfLines = 0; NSString *currency = self.item.currency; // Special case for GBX (0,01 GBP) if ([currency isEqualToString:@"GBX"]) { currency = @"GBP"; } if (![currency isEqualToString:[[ZENGlobalSettings sharedStore] portfolioCurrency]]) { // 6 sections switch (section) { case 0: numberOfLines = 1; // number of shares break; case 1: numberOfLines = 2; // purchase & current share price break; case 2: numberOfLines = 2; // intraday change value & percentage break; case 3: numberOfLines = 2; // purchase & current currency rate break; case 4: numberOfLines = 2; // purchase & current value break; case 5: numberOfLines = 2; // gain or loss value & percentage break; default: NSLog(@"Unknown section"); break; } } else { // 5 sections only switch (section) { case 0: numberOfLines = 1; // number of shares break; case 1: numberOfLines = 2; // purchase & current share price break; case 2: numberOfLines = 2; // intraday change value & percentage break; case 3: numberOfLines = 2; // purchase & current value break; case 4: numberOfLines = 2; // gain or loss value & percentage break; default: NSLog(@"Unknown section"); break; } } return numberOfLines; } |
Getting a bit ugly …
cell For Row At Index Path:
Sorry, but I really have to paste that here, for the sake of the demonstration …
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"StockDetailCell"]; // Removed a bunch of code here ... if (indexPath.section == 0) { // section "number of shares" if (indexPath.row == 0) { // single line cell.textLabel.text = blablabla; cell.detailTextLabel.text = blablabla; } } else if (indexPath.section == 1) { // section "share price" if (indexPath.row == 0) { // purchase share price cell.textLabel.text = blablabla; cell.detailTextLabel.text = blablabla; } else if (indexPath.row == 1) { // current share price cell.textLabel.text = blablabla; cell.detailTextLabel.text = blablabla; } } else if (indexPath.section == 2) { // section "intraday evolution" // More blablabla, this section is not so interesting for our purpose ... } else if (indexPath.section == 3 && (![currency isEqualToString:[[ZENGlobalSettings sharedStore] portfolioCurrency]])) { // section "currency rate" // this section should not be displayed if the item currency equals the portfolio currency if (indexPath.row == 0) { // purchase currency rate cell.textLabel.text = blablabla; cell.detailTextLabel.text = blablabla; } else if (indexPath.row == 1) { // current currency rate cell.textLabel.text = blablabla; cell.detailTextLabel.text = blablabla; } } else if ((indexPath.section == 4 && (![currency isEqualToString:[[ZENGlobalSettings sharedStore] portfolioCurrency]])) || (indexPath.section == 3 && ([currency isEqualToString:[[ZENGlobalSettings sharedStore] portfolioCurrency]]))) { // section "stock valuation" if (indexPath.row == 0) { // cost of stock (purchase valuation) in portfolio currency cell.textLabel.text = blablabla; cell.detailTextLabel.text = blablabla; } else if (indexPath.row == 1) { // value of stock (current valuation) in portfolio currency cell.textLabel.text = blablabla; cell.detailTextLabel.text = blablabla; } } else if ((indexPath.section == 5 && (![currency isEqualToString:[[ZENGlobalSettings sharedStore] portfolioCurrency]])) || (indexPath.section == 4 && ([currency isEqualToString:[[ZENGlobalSettings sharedStore] portfolioCurrency]]))) { // section "gain or loss" if (indexPath.row == 0) { // gain or loss value cell.textLabel.text = blablabla; cell.detailTextLabel.text = blablabla; } else if (indexPath.row == 1) { // gain or loss percentage cell.textLabel.text = blablabla; cell.detailTextLabel.text = blablabla; } } return cell; } |
See the issue there? At one point, I needed to add into that kind of stuff:
if (the indexPath.section is x AND the stock currency is different from the user’s portfolio currency) OR (the indexPath.section is x-1 AND the stock currency is the same as the user’s portfolio currency)
That’s ugly, repetitive coding, and very error-prone (you can trust me on this).
In Swift
So, what exactly did Paul Hegart mean when he wrote:
Don’t forget about Swift features like enum. Use Swift to its fullest.
I think he had in mind something like this:
- First, create an Enum for each section and each item that you would like to populate in your table view
1234567891011121314151617181920212223private enum SectionType {case Generalcase Pricecase Intradaycase CurrencyRatecase Valuationcase GainOrLoss}private enum Item {case NumberOfSharescase DatePurchasecase PricePurchasecase PriceCurrentcase IntradayValuecase IntradayPercentagecase CurrencyRatePurchasecase CurrencyRateCurrentcase ValuationPurchasecase ValuationCurrentcase GainOrLossValuecase GainOrLossPercentage} - Then create a Struct to describe a section
1234private struct Section {var type: SectionTypevar items: [Item]}
That means that each section has a type, and includes a list of items. - Finally, create a var for the sections
1private var sections = [Section]()
Now let’s build the table structure:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// MARK: - Public properties var stock: Stock? { didSet { if currency == GlobalSettings.sharedStore.portfolioCurrency { sections = [ Section(type: .General, items: [.NumberOfShares, .DatePurchase]), Section(type: .Price, items: [.PricePurchase, .PriceCurrent]), Section(type: .Intraday, items: [.IntradayValue, .IntradayPercentage]), Section(type: .Valuation, items: [.ValuationPurchase, .ValuationCurrent]), Section(type: .GainOrLoss, items: [.GainOrLossValue, .GainOrLossPercentage]) ] } else { // currency != GlobalSettings.sharedStore.portfolioCurrency sections = [ Section(type: .General, items: [.NumberOfShares, .DatePurchase]), Section(type: .Price, items: [.PricePurchase, .PriceCurrent]), Section(type: .Intraday, items: [.IntradayValue, .IntradayPercentage]), Section(type: .CurrencyRate, items: [.CurrencyRatePurchase, .CurrencyRateCurrent]), Section(type: .Valuation, items: [.ValuationPurchase, .ValuationCurrent]), Section(type: .GainOrLoss, items: [.GainOrLossValue, .GainOrLossPercentage]) ] } } } |
I created the table structure in the didSet of my public property « stock », but you could do that in viewDidLoad: as well.
Notice how readable that is now.
What do our data source functions become?
number Of Sections:
1 2 3 |
override func numberOfSectionsInTableView(tableView: UITableView) -> Int { return sections.count } |
One-liner, as promised.
number Of Rows In Section:
1 2 3 4 |
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return sections[section].items.count } |
Again, one line of code.
titleForHeaderInSection
I add this one here, just to show that I created my Section struct with a type, in order to handle section titles in a very simple way.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { var sectionLocalizedString = String() switch sections[section].type { case .General: return "\(stock!.name) - \(stock!.market)" case .Price: return "Share price" // I simplified; this text was localized of course case .Intraday: return "Intraday evolution" case .CurrencyRate: return "Currency rate" case .Valuation: return "Stock valuation" case .GainOrLoss: return "Gain or loss" } } |
cell For Row At Index Path:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { var cell = tableView.dequeueReusableCellWithIdentifier("StockDetailCell", forIndexPath: indexPath) as! UITableViewCell switch sections[indexPath.section].items[indexPath.row] { case .NumberOfShares: cell.textLabel?.text = blabla cell.detailTextLabel?.text = blabla case .DatePurchase: cell.textLabel?.text = blabla cell.detailTextLabel?.text = blabla case .PricePurchase: cell.textLabel?.text = blabla cell.detailTextLabel?.text = blabla case .PriceCurrent: cell.textLabel?.text = blabla cell.detailTextLabel?.text = blabla case .IntradayValue: cell.textLabel?.text = blabla cell.detailTextLabel?.text = blabla case .IntradayPercentage: cell.textLabel?.text = blabla cell.detailTextLabel?.text = blabla case .CurrencyRatePurchase: cell.textLabel?.text = blabla cell.detailTextLabel?.text = blabla case .CurrencyRateCurrent: cell.textLabel?.text = blabla cell.detailTextLabel?.text = blabla case .ValuationPurchase: cell.textLabel?.text = blabla cell.detailTextLabel?.text = blabla case .ValuationCurrent: cell.textLabel?.text = blabla cell.detailTextLabel?.text = blabla case .GainOrLossValue: cell.textLabel?.text = blabla cell.detailTextLabel?.text = blabla case .GainOrLossPercentage: cell.textLabel?.text = blabla cell.detailTextLabel?.text = blabla } return cell } |
This method is the core of the table view display, so it’s still quite long. But it’s really simple to implement.
And the most obvious advantage is that now, I don’t have to manage the order of the sections or the number of cells here. Everything is defined by my sections var. If I wanted to change the order of my sections, or add new items in a section, it would take me 10 seconds.
Note also that, although in this case a unique reusable cell is « dequeued » at the beginning of the function, this model is perfectly compatible with different types of cells. For that you could just dequeue a different cell type (different subclass of UITableViewCell, different reuse identifier) in each « case » declaration.
In that case and many others, I can confirm that Swift made my code more elegant, more simple, and more reliable.
I hope this technique will help you improve your table view structure, too!
Hi,
That’s a very good article. We could even enhance the code à little more ti have à really DRY code.
For instance this code :
// MARK: – Public properties
var stock: Stock? {
didSet {
if currency == GlobalSettings.sharedStore.portfolioCurrency {
sections = [
Section(type: .General, items: [.NumberOfShares, .DatePurchase]),
Section(type: .Price, items: [.PricePurchase, .PriceCurrent]),
Section(type: .Intraday, items: [.IntradayValue, .IntradayPercentage]),
Section(type: .Valuation, items: [.ValuationPurchase, .ValuationCurrent]),
Section(type: .GainOrLoss, items: [.GainOrLossValue, .GainOrLossPercentage])
]
} else { // currency != GlobalSettings.sharedStore.portfolioCurrency
sections = [
Section(type: .General, items: [.NumberOfShares, .DatePurchase]),
Section(type: .Price, items: [.PricePurchase, .PriceCurrent]),
Section(type: .Intraday, items: [.IntradayValue, .IntradayPercentage]),
Section(type: .CurrencyRate, items: [.CurrencyRatePurchase, .CurrencyRateCurrent]),
Section(type: .Valuation, items: [.ValuationPurchase, .ValuationCurrent]),
Section(type: .GainOrLoss, items: [.GainOrLossValue, .GainOrLossPercentage])
]
}
}
}
Could have been like this :
// MARK: – Public properties
var stock: Stock? {
didSet {
sections.append(Section(type: .General, items: [.NumberOfShares, .DatePurchase]))
sections.append(Section(type: .Price, items: [.PricePurchase, .PriceCurrent]))
sections.append(Section(type: .Intraday, items: [.IntradayValue, .IntradayPercentage]))
if currency == GlobalSettings.sharedStore.portfolioCurrency {
sections.append(Section(type: .CurrencyRate, items: [.CurrencyRatePurchase, .CurrencyRateCurrent]))
}
sections.append(Section(type: .Valuation, items: [.ValuationPurchase, .ValuationCurrent]))
sections.append(Section(type: .GainOrLoss, items: [.GainOrLossValue, .GainOrLossPercentage]))
}
}
Cheers !
That’s a very good point. Thank you!
This is very interesting. We already do that here in our code but in more deeper way.
For example you could use function inside the enums, and within those functions do the switch case around self. In the end your view controller could look like this:
override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return sections[section].type.title()
}
Great idea! I followed a similar approach in Objective-C with the NS_ENUM macro.
One more step in DRYing up your cell creation method is to use a presenter. Have your method create the cell then pass in the object and cell to a presenter object.
To make this work the object will need to conform to a protocol. This protocol will have to getters, title and detail for example, that return the strings. Then the presenter can naively pass the corresponding attribute to the cell’s labels.
It’s a good article, I like it very much!
I’ve think around table view’s data structure many times,
And I can give another solution, the idea comes from CS193p,
, like this:
class DataSourceAndDelegate: NSObject, UITableViewDataSource, UITableViewDelegate {
var sections: [(sectionInfo: SectionInfo, cellInfos: [CellInfo])] = [] { didSet { tableView.reloadData() } }
weak var tableView: UITableView! { didSet { tableView.dataSource = self; tableView.delegate = self } }
var reuseIdentifierForCellInfo: ((CellInfo) -> String)!
var configureCellForCellInfo: ((UITableViewCell, CellInfo) -> Void)?
var titleForSectionInfo: ((SectionInfo) -> String?)?
var didSelectCellInfo: ((CellInfo) -> Void)?
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return sections.count
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sections[section].cellInfos.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellInfo = cellInfoAtIndexPath(indexPath)
let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifierForCellInfo(cellInfo))!
configureCellForCellInfo?(cell, cellInfo)
return cell
}
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return titleForSectionInfo?(sections[section].sectionInfo)
}
// Helper
func cellInfoAtIndexPath(indexPath: NSIndexPath) -> CellInfo {
return sections[indexPath.section].cellInfos[indexPath.item]
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.deselectRowAtIndexPath(indexPath, animated: true)
let cellInfo = cellInfoAtIndexPath(indexPath)
didSelectCellInfo?(cellInfo)
}
}
And in ViewController use it like this:
class ViewController: UIViewController {
@IBOutlet weak var tableView: UITableView! { didSet { dataSourceAndDelegate.tableView = tableView } }
var dataSourceAndDelegate = DataSourceAndDelegate()
override func viewDidLoad() {
super.viewDidLoad()
setupDataSourceAndDelegate()
}
func setupDataSourceAndDelegate() {
dataSourceAndDelegate.sections = [
(.OverView, [
.Name,
.Detail,
.Time]),
(.Author, [
.AuthorName,
.AuthorImage,
.AuthorAge]),
(.Footer, [
.LikeNumber,
.FollowNumer])
]
dataSourceAndDelegate.reuseIdentifierForCellInfo = {
cellInfo in
return « cell »
}
dataSourceAndDelegate.configureCellForCellInfo = {
cell, cellInfo in
cell.textLabel?.text = cellInfo.rawValue
}
dataSourceAndDelegate.titleForSectionInfo = {
sectionInfo in
return sectionInfo.rawValue
}
dataSourceAndDelegate.didSelectCellInfo = {
cellInfo in
print(« did Select: \(cellInfo.rawValue) »)
}
}
enum SectionInfo: String {
case OverView
case Author
case Footer
}
enum CellInfo: String {
case Name
case Detail
case Time
case AuthorName
case AuthorImage
case AuthorAge
case LikeNumber
case FollowNumer
}
}
DataSourceAndDelegate change table view’s delegate base API to block base API, And in ViewController we only provide minimal configuration to table view, the cell is configured from cell enum, section is configured from section enum.
Cheers !
Sorry the code above not show placeholder type defined on DataSourceAndDelegate which is « SectionInfo » and « CellInfo ».
DataSourceAndDelegate is like a swift Dictionary when you init it, should give two specific Type.
Absolutely stunning…exactly what I searched Google hoping to find!
Thank you! 🙂
I’ll likely write up a blog post about implementing this some time and link back to your site. 😀