0

I need to build a UITableView with heterogenous datasources example two models: person and city.

One option is to combine array of person and array of city into an array of AnyObject and check the type of object stored in cell.

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let someobject = self.events[indexPath.row]

    var test = self.events
    if someobject is Person  {
        print ("I am a person")
    } 
    if someobject is City  {
        print ("I am a city")
    }
} 

Another option is to create a base class and make Person and City inherit from Base:

 class Person: Base {}
 class City: Base {}

then instead of [AnyObject] use [Base] for tableView datasource.

I wonder if there is a better way to do this?

2 Answers 2

2

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.

Sign up to request clarification or add additional context in comments.

4 Comments

I like this approach overall, but I can hear legions of purists reel in horror at mixing models and views...
What mixing do you mean?
Having the Person and City model classes responsible for vending their table view cells. It would make more sense for the cells to configure themselves with a Person or City object that's passed to them; then you'd have an MVVM approach.
But in a use case as simple as this one, I wouldn't object to seeing an array of [Any] holding Citys and Persons, without forcing them both to adopt a common protocol, and doing a simple cast to determine which cell is appropriate.
0

A couple things you could do:

  1. Make a protocol and have them both conform to it, so you don't have superclass issues, and still have the same type.
  2. Just do [AnyObject], and then have your cellForRowAtIndexPath do this:

    let object = data[indexpath.row]
    if let city = object as? City {
        //do something
    }else if let...
    

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.