Structure your UITableView better with structs and enums in Swift

IMG_0424.PNG

Structure your UITableView better with structs and enums in Swift

26 août, 2015
Frédéric ADDA
, ,
8 comments

For my first post, I wanted to 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.

General view of your stocks

General view of your stocks

 

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:

Detail view of a stock

Detail view of a stock

This table view contains the following sections:

  1. General (number of shares and purchase date)
  2. Share price (purchase / current price)
  3. Stock valuation in the stock currency (purchase cost / current stock value)
  4. Currency rate (purchase / current rate) *
  5. Stock valuation in the user’s currency (purchase cost / current stock value)
  6. 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:

 

number Of Rows In Section:

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 …

 

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:

 

  1. First, create an Enum for each section and each item that you would like to populate in your table view
  2. Then create a Struct to describe a section

    That means that each section has a type, and includes a list of items.
  3. Finally, create a var for the sections

     

Now let’s build the table structure:

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:

One-liner, as promised.

 

number Of Rows In Section:

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.

 

 

cell For Row At Index Path:

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!

8 Comments

Abdou Slayne  septembre 9, 2015 at 3:14

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 !

    novaeramain  septembre 9, 2015 at 3:32

    That’s a very good point. Thank you!

kevin delord  septembre 30, 2015 at 11:35

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()
}

Joe Masilotti  décembre 10, 2015 at 5:09

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.

Luo Jie  janvier 16, 2016 at 12:37

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 !

Luo Jie  janvier 16, 2016 at 12:45

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.

Justin Stanley  janvier 31, 2016 at 10:48

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. 😀

Enumified TableView with Dynamic Prototype Cells in Swift | swift (with.justin)  février 6, 2016 at 4:16

[…] stumbled upon this amazing post by Frédéric ADDA over at Nova Era called Structure Your UITableView Better with Structs and Enums in Swift as I was searching for a way to refactor the Settings page in my app, BB Links, to make it easier […]

leave A Comment

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *