It seems like it’s almost a given that when using a TableView in iOS, the ViewController should be its data source. This is not the best solution! Here’s why:
It might seem like the TableView is within a ViewController, and that coupling them together is not a concern, but consider this: You’re making a dynamic settings screen, and you have a TableView displaying different settings. But what if you want to segue to a screen that shows a more narrow subsection of the screens, where you show a different ViewController but want the TableView to behave the same? The right answer is not to copy paste code to from one ViewController to the other!
Rarely are you presenting data in its raw form. At the very least, you are concatenating strings before you display a bit of text in the cell. If you have a bunch of TableViews with the same type of data, each within a different ViewController, you will have a rough time trying to change the way you present you data. Think of the localisation!
If there’s one thing I hate, it’s the boring task of writing code I’ve written a million times before. But consider this: You can totally write a generic TableViewDataSource, that will be good enough for 90% of the TableViews in your average app!
Try writing unit tests for a UITableViewController which is the data source of its TableView. I double dare you. I can’t emphasise enough how much making the ViewController as thin as possible will reduce your stress levels when it comes to testing.
So how do you go about making a nice, clean, testable UITableViewDataSource?
What if we could have a UITableViewDataSource class that we would only need to supply data to, and it would provide everything for us. Sounds good, right?
We’ll make a dynamic settings screen for our app. Settings screens usually have different segues to similar ViewControllers, with a similar TableView, so it’s a perfect candidate for a reusable TableViewDataSource!
In order to make our DataSource reusable and easily modified, we’ll create an enum which will represent the data for a single cell in our app.
enum SettingType {
case Switch(text: String) //on-off
case Segue(text: String) //navigate to a sub-section
case Info(text: String, detail: String) //e.g. usage stats
var identifier: String {
switch self {
case .Info: return "infoCell"
case .Segue: return "segueCell"
case .Switch: return "switchCell"
}
}
}
This has everything our data source would need, including the cell-reuse identifiers, which I like to provide as a computed variable because we only need to use the error-prone strings once. (Another way to deal with this is to represent the identifiers as an enum.)
We’ll also create a struct to represent a section in our TableView, which will hold the necessary SettingType objects and the section title.
struct SettingsSection {
var title: String?
var cellData: [SettingType]
init(title: String?, cellData: [SettingType]) {
self.title = title
self.cellData = cellData
}
}
Now that we have clear value types that represent our data, we can create our actual data source. It only needs to have one property we’ll call sections, *which is an array of *SettingsSection objects.
class SettingsTableViewDataSource {
var sections: [SettingsSection]
init(sections: [SettingsSection]) {
self.sections = sections
}
}
But this is not a UITableViewDataSource just yet. In order to do that, we need to subclass NSObject (because Objective-C) and implement the UITableViewDataSource protocol, as well as the required protocol methods.
class SettingsTableViewDataSource: NSObject, UITableViewDataSource {
//... (above)
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return sections.count
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sections[section].cellData.count
}
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return sections[section].title
}
This a run of the mill implementation of these methods, where we’re just returning the count and title of our sections.
And finally, the meat of the data source:
class SettingsTableViewDataSource: NSObject, UITableViewDataSource {
//... (above)
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let setting: SettingType = sections[indexPath.section].cellData[indexPath.row]
if let cell = tableView.dequeueReusableCellWithIdentifier(setting.identifier) {
switch setting {
case .Switch(text: let text):
cell.textLabel?.text = text
cell.accessoryType = .DetailButton
cell.accessoryView = UISwitch()
case .Segue(text: let text):
cell.textLabel?.text = text
cell.accessoryType = .DisclosureIndicator
case .Info(text: let text, detail: let detailText):
cell.textLabel?.text = text
cell.detailTextLabel?.text = detailText
}
return cell
} else {
fatalError("Unknown identifier")
}
}
}
Again, this is a fairly simple implementation, albeit a bit longer than the methods above. All we’re doing is grabbing the SettingType for the current cell index, and returning a configured cell with the correct identifier.
This is all we need for a reusable UITableViewDataSource! If you want, you can extend this class to have methods for adding or removing data, and you can add a callback to be called via a property observer of the sections property so you can call tableView.reloadData() when it changes, etc.
Now that we have our data source, let’s create our settings screen. We can do this with only 6 lines of code within the actual ViewController class. Don’t believe me?
This is the data provided to us from the Model: (Note that this is just some stub model for this example. Since I’m focusing on the UIViewController, where the data comes from doesn’t actually matter.)
struct Model {
static let data = [
SettingsSection(title: "General",
cellData: [SettingType.Switch(text: "Dark mode"),
SettingType.Switch(text: "Auto save"),
SettingType.Segue(text: "Language")]),
SettingsSection(title: "Stats",
cellData: [SettingType.Info(text: "Usage", detail: "2 days")])
]
}
I prefer to use the Interface Builder, so I’ve added a TableView with three prototype cells with the correct identifiers for our SettingTypes. The info cell’s style is Right Detail, *and the other ones are a *Basic cell.
And this is actual View Controller code:
class MainSettingsViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
let dataSource = SettingsTableViewDataSource(sections: Model.data)
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.dataSource = dataSource
}
}
And it works!
Now you have a thin ViewController and a nice UITableViewDataSource that you can test, extend, and reuse.
Another cool thing about this data source is that you can ask it for the actual SettingType object, and perform a switch on it when providing logic to the TableView. This is great because you can easily add logic specific to the current cell without guessing what the cell is, and add different cell types easily.
Enjoy!
(This article was inspired by *Andy Matuschak’s talk in which he refactored a horrible ViewController)*