Yaron Levi
Yaron Levi

Reputation: 13077

Swift - How to use dequeueReusableCellWithIdentifier with two cell types? (identifiers)

I am trying to build a table view for events, like so:
enter image description here

I have two cell prototypes:

Also, I have this class to represent a date:

class Event{

    var name:String = ""
    var date:NSDate? = nil
}

And this is the table controller:

class EventsController: UITableViewController {

    //...

    var eventsToday = [Event]()
    var eventsTomorrow = [Event]()
    var eventsNextWeek = [Event]()

    override func viewDidLoad() {
        super.viewDidLoad()

        //...

        self.fetchEvents()//Fetch events from server and put each event in the right property (today, tomorrow, next week)

        //...
    }

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

        let event = tableView.dequeueReusableCellWithIdentifier("event", forIndexPath: indexPath) as EventTableViewCell
        let seperator = tableView.dequeueReusableCellWithIdentifier("seperator", forIndexPath: indexPath) as SeperatorTableViewCell

        //...

        return cell
    }
}

I have all the information I need at hand, but I can't figure out the right way to put it all together. The mechanics behind the dequeue func are unclear to me regrading multiple cell types.

I know the question's scope might seem a little too broad, but some lines of code to point out the right direction will be much appreciated. Also I think it will benefit a lot of users since I didn't found any Swift examples of this.

Thanks in advance!

Upvotes: 1

Views: 2730

Answers (1)

Rob
Rob

Reputation: 438307

The basic approach is that you must implement numberOfRowsInSection and cellForRowAtIndexPath (and if your table has multiple sections, numberOfSectionsInTableView, too). But each call to the cellForRowAtIndexPath will create only one cell, so you have to do this programmatically, looking at the indexPath to determine what type of cell it is. For example, to implement it like you suggested, it might look like:

override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    return 1
}

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return eventsToday.count + eventsTomorrow.count + eventsNextWeek.count + 3 // sum of the three array counts, plus 3 (one for each header)
}

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    var index = indexPath.row

    // see if we're the "today" header

    if index == 0 {
        let separator = tableView.dequeueReusableCellWithIdentifier("separator", forIndexPath: indexPath) as SeparatorTableViewCell

        // configure "today" header cell

        return separator
    }

    // if not, adjust index and now see if we're one of the `eventsToday` items

    index--

    if index < eventsToday.count {
        let eventCell = tableView.dequeueReusableCellWithIdentifier("event", forIndexPath: indexPath) as EventTableViewCell
        let event = eventsToday[index]

        // configure "today" `eventCell` cell using `event`

        return eventCell
    }

    // if not, adjust index and see if we're the "tomorrow" header

    index -= eventsToday.count

    if index == 0 {
        let separator = tableView.dequeueReusableCellWithIdentifier("separator", forIndexPath: indexPath) as SeparatorTableViewCell

        // configure "tomorrow" header cell

        return separator
    }

    // if not, adjust index and now see if we're one of the `eventsTomorrow` items

    index--

    if index < eventsTomorrow.count {
        let eventCell = tableView.dequeueReusableCellWithIdentifier("event", forIndexPath: indexPath) as EventTableViewCell
        let event = eventsTomorrow[index]

        // configure "tomorrow" `eventCell` cell using `event`

        return eventCell
    }

    // if not, adjust index and see if we're the "next week" header

    index -= eventsTomorrow.count

    if index == 0 {
        let separator = tableView.dequeueReusableCellWithIdentifier("separator", forIndexPath: indexPath) as SeparatorTableViewCell

        // configure "next week" header cell

        return separator
    }

    // if not, adjust index and now see if we're one of the `eventsToday` items

    index--

    assert (index < eventsNextWeek.count, "Whoops; something wrong; `indexPath.row` is too large")

    let eventCell = tableView.dequeueReusableCellWithIdentifier("event", forIndexPath: indexPath) as EventTableViewCell
    let event = eventsNextWeek[index]

    // configure "next week" `eventCell` cell using `event`

    return eventCell
}

Having said that, I really don't like that logic. I'd rather represent the "today", "tomorrow" and "next week" separator cells as headers, and use the section logic that table views have.

For example, rather than representing your table as a single table with 8 rows in it, you could implement that as a table with three sections, with 2, 1, and 2 items in each, respectively. That would look like:

override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    return 3
}

override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
    switch section {
    case 0:
        return "Today"
    case 1:
        return "Tomorrow"
    case 2:
        return "Next week"
    default:
        return nil
    }
}

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    switch section {
    case 0:
        return eventsToday.count
    case 1:
        return eventsTomorrow.count
    case 2:
        return eventsNextWeek.count
    default:
        return 0
    }
}

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let eventCell = tableView.dequeueReusableCellWithIdentifier("event", forIndexPath: indexPath) as EventTableViewCell

    var event: Event!

    switch indexPath.section {
    case 0:
        event = eventsToday[indexPath.row]
    case 1:
        event = eventsTomorrow[indexPath.row]
    case 2:
        event = eventsNextWeek[indexPath.row]
    default:
        event = nil
    }

    // populate eventCell on the basis of `event` here

    return eventCell
}

The multiple section approach maps more logically from the table view to your underlying model, so I'd to adopt that pattern, but you have both approaches and you can decide.

Upvotes: 4

Related Questions