Swarup Ghosh
Swarup Ghosh

Reputation: 55

How to display UITableView Cells category wise?

I want a UITableView to display the following data:

required output

Instead of:

my current output

The code I've used is as follows:

// Data model class defining an Animal
class Animal {

    // data members
    var name: String
    var type: String

    // initializer
    init(name: String, type: String) {
        self.name = name
        self.type = type
    }

}

and

// View Controller
import UIKit

class AnimalsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    // an array of Animal(s)
    var animals: [Animal] = [Animal("Cat", "Domestic"), Animal("Dog", "Domestic"), Animal("Lion", "Wild"), Animal("Deer", "Wild"), Animal("Tiger", "Wild")];

    override func viewDidLoad() {
        super.viewDidLoad();

    }

    // returns the number of rows to be present in table
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return animals.count
    }

    // returns each cell to be present in the table
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cellIdentfier = "animalbasic"
        // uses a protype cell from storyboard
        let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentfier)
        cell.textLabel.text = animals[indexPath.row].name
        return cell
    }
}

What do I need to change, in my code, in order to get the required output?(Assume an array is always sorted Animal.type wise)

Upvotes: 1

Views: 138

Answers (2)

Igor M
Igor M

Reputation: 474

Suggestions: use a struct for this type of things, rather than a class for expendability I would use an enum for the animal type, something like:

enum AnimalType: String {
 case domestic = "Domestic"
 case wild = "Wild"
}

Note: not actually needed to make it string, you can also have a function that based on the type would return a localised string, you can write something like

enum AnimalType: String {
 case domestic, wild
 var localizedString: String {
    switch self {
    case .domestic: return "Domestic"
    case .wild: return "Wild"
    }
}

Now your struct will look like this

struct Animal {
    let name: String
    let type: AnimalType
}

and now you are able to init it like this

let animal = Animal(name: "Cat", type: .domestic)

Now back your problem, I would suggest to keep the type in a section header rather than a cell, it would make more sense don't you think?

Create a variable let sections = [.domestic, .wild] now you have the count and it will also be easily extended

func numberOfSections(in tableView: UITableView) -> Int {
    return sections.count
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    let sectionType = sections[section]

    switch sectionType {
    case .domestic:
        return animals.filter{ $0.type == .domestic }.count
    case .wild:
        return animals.filter{ $0.type == .wild }.count
    default:
        return 0
    }
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let sectionType = sections[section]

    switch sectionType {
    case .domestic:
        /// Configure your cell
    case .wild:
        /// Configure your cell
    default:
        assertionFailure("Should not reach in this situation")
        return UITableViewCell()
    }
}

ofcourse you will have to also configure the header for your sections:

func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
    let sectionType = sections[section]

    switch sectionType {
    default: sectionType.localizedString
}

Your approach is also doable, but this would mean that at numberOfRowsInSection you will have to add the number of sections:

// returns the number of rows to be present in table
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return animals.count + sections.count
}

and then cellForRowAt would be a pain to mentain, you would need to do some calculations to see what row is it, is it still wild domestic, is it wild yet

TLTR you would have a bunch of if, if else, if else, else, and by only adding one more type (dinosaur maybe) this pain will hit you back

Note: this code can be improved using a viewModel, but for the sake of being fast i skipped that part

Upvotes: 1

rob mayoff
rob mayoff

Reputation: 385650

You need to break your data into sections by type.

class AnimalsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    var animals: [Animal] = [Animal("Cat", "Domestic"), Animal("Dog", "Domestic"), Animal("Lion", "Wild"), Animal("Deer", "Wild"), Animal("Tiger", "Wild")]

    var sections: [[Animal]] = []

    override func viewDidLoad() {
        super.viewDidLoad();

        var sectionForType = [String: [Animal]]()
        for animal in animals {
            if sectionForType[animal.type] == nil { sectionForType[animal.type] = [] }
            sectionForType[animal.type]!.append(animal)
        }
        sections = sectionForType.keys.sorted().map({ sectionForType[$0]! })
    }

    func numberOfSections(in tableView: UITableView) -> Int {
        return sections.count
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return sections[section].count
    }

    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return sections[section][0].type
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "animalbasic")!
        cell.textLabel!.text = sections[indexPath.section][indexPath.row].name
        return cell
    }
}

Upvotes: 3

Related Questions