I would do it like this. You have your model classes:
class Person {
// ...
}
class City {
// ...
}
I assume your data is a mixed sequence of Person and City where each element in the sequence might be either a Person or a City.
And presumably you have one UITableViewCell subclass for each model, like this:
class PersonCell: UITableViewCell {
var person: Person? {
didSet {
guard let person = person else { return }
// update appearance using properties of person
}
}
}
class CityCell: UITableViewCell {
var city: City? {
didSet {
guard let city = city else { return }
// update appearance using properties of city
}
}
}
And I assume you've registered each cell type with the table view, either by creating the cells as prototypes in your storyboard, or by calling registerNib:forCellReuseIdentifier: or registerClass:forCellReuseIdentifier: on the table view.
What you need at this point is a way to put both Person objects and City objects in an array, and to get a cell of the appropriate type for each data object, and configure the cell with the data object. Let's make a protocol for that:
protocol TableViewDatum: class {
/// Return a fully-configured cell for displaying myself in the table view.
func cell(inTableView tableView: UITableView, forIndexPath indexPath: NSIndexPath) -> UITableViewCell
}
Then we extend Person to conform to the protocol:
extension Person: TableViewDatum {
func cell(inTableView tableView: UITableView, forIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Person", forIndexPath: indexPath) as! PersonCell
cell.person = self
return cell
}
}
And we extend City to conform to the protocol:
extension City: TableViewDatum {
func cell(inTableView tableView: UITableView, forIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("City", forIndexPath: indexPath) as! CityCell
cell.city = self
return cell
}
}
At this point, implementing the required UITableViewDataSource methods is trivial:
class MyTableViewDataSource: NSObject, UITableViewDataSource {
private var data = [TableViewDatum]()
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
return data[indexPath.row].cell(inTableView: tableView, forIndexPath: indexPath)
}
}
Note 1.
You could reduce the duplication of code between the Person extension and the City extension with generics, but it's probably not worth it. It would certainly be harder to understand and wouldn't save much code.
Note 2.
If you don't have a custom cell class for, say, Person, and you're just using one of the standard cell styles like UITableViewCellStyle.Subtitle, then you can still use this pattern. You just configure the cell in the Person extension instead of in the custom cell class. Example:
extension Person: TableViewDatum {
func cell(inTableView tableView: UITableView, forIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Person", forIndexPath: indexPath)
// Assume cell style is .Subtitle.
cell.textLabel?.text = self.name
cell.detailTextLabel?.text = self.title
return cell
}
}
Note 3.
In response to commenter NRitH's concern that I am “mixing models and views” by “[h]aving the Person and City model classes responsible for vending their table view cells”: I am not mixing models and views. The extensions that make Person and City conform to TableViewDatum, and thus vend their cells, are quite separate from the Person and City class definitions.
It is easy to imagine the Person and City classes coming from some third-party framework. For example, replace Person with CNContact (from the iOS Contacts framework) and City with CLLocation (from the iOS CoreLocations framework).
This pattern works just as well in that scenario: the class implementations provided by the frameworks know nothing about my app's user interface. It's not reasonable to claim that the class implementations are vending their cells. It is the extensions that are vending the cells.
For more on this implementation style, watch WWDC 2015 Session 408: Protocol-Oriented Programming in Swift.
Regarding “It would make more sense for the cells to configure themselves with a Person or City object that's passed to them”, that is precisely what I've done! I set the person property of PersonCell to make PersonCell configure itself, and I set the city property of CityCell to make CityCell configure itself.
Presumably you just want to eliminate any passing through the model object itself. But as I said, the extensions are quite separate from the model objects so I don't consider them problematic.
Nevertheless, let's see what your way looks like. Here's what I think you mean:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
switch data[indexPath.row] {
case let person as Person:
let cell = tableView.dequeueReusableCellWithIdentifier("Person", forIndexPath: indexPath) as! PersonCell
cell.person = person
return cell
case let city as City:
let cell = tableView.dequeueReusableCellWithIdentifier("City", forIndexPath: indexPath) as! CityCell
cell.city = city
return cell
default: fatalError()
}
}
You have a switch statement. (You could use a cascade of if statements if you prefer but it's the same basic pattern.) If you implement any other row-specific UITableViewDataSource or UITableViewDelegate methods (e.g. tableView:canEditRowAtIndexPath:, tableView:editActionsForRowAtIndexPath:, etc.), you probably need a similar switch statement in each one. Did you add a new model class but forget to update one of those delegate methods? Oops, you won't find out until run time. In my design, you add a corresponding required method to the TableViewDatum protocol, and the data source or delegate method calls that protocol method. If you forget to implement the method in one of your model classes, the compiler flags an error.
That's also why my design doesn't have anything corresponding to the default: fatalError() case. The compiler-enforced type safety rules it out at compile time.