0

I want to filter items with property isCompleted = true to section with name Completed and non completed items to ToDo. How to render items?

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if section == 0 {
            return manager.tasks.filter({$0.isCompleted == false}).count
        } else {
            return manager.tasks.filter({$0.isCompleted}).count
        }
    }

    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        switch section {
        case 0:
            return "ToDo"
        case 1:
            return "Completed"
        default:
            return nil
        }
    }

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: Keys.cell.rawValue, for: indexPath) as! ToDoCell

        let currentItem = manager.tasks[indexPath.row]
        cell.titleLabel.text = currentItem.taskName
        cell.descriptionLabel.text = currentItem.description
        if manager.tasks[indexPath.row].description?.isEmpty ?? false {
            cell.descLabelBottomConstraint.constant = 0
        }

        let accessoryType: UITableViewCell.AccessoryType = currentItem.isCompleted ? .checkmark : .none
        cell.accessoryType = accessoryType
        return cell
    }

I guess I need to filter items into two different arrays? But which way is the most correct?

SilentKunZ
  • 75
  • 8
  • `let currentItem = manager.tasks[indexPath.row]` is the problem. You are failing to distinguish completed items. The entire data model itself is wrong. A single simple array won’t work. – matt Feb 27 '20 at 13:15

3 Answers3

1

You can create 2 properties completed and notCompleted in the Manager and use them as dataSource of the tableView.

class Manager {    
    lazy var completed: [Task] = {
        return tasks.filter({ !$0.isCompleted })
    }()
    lazy var notCompleted: [Task] = {
        return tasks.filter({ $0.isCompleted })
    }()
}

UITableViewDataSource and UITableViewDelegate methods,

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return section == 0 ? manager.notCompleted.count : manager.completed.count
}

func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
    return section == 0 ? "Todo" : "Completed"
}
PGDev
  • 23,751
  • 6
  • 34
  • 88
1

Never filter things in numberOfRowsInSection. Don't do that, this method is called very often.

  • Create a model

    struct Section {
        let title : String
        var items : [Task]
    }
    
  • Declare the data source array

    var sections = [Section]()
    
  • In viewDidLoad populate the array and reload the table view

    sections = [Section(title: "ToDo", items: manager.tasks.filter{!$0.isCompleted}),
                Section(title: "Completed", items: manager.tasks.filter{$0.isCompleted})]
    tableView.reloadData()
    
  • Now the datasource methods become very clean (and fast)

    override func numberOfSections(in tableView: UITableView) -> Int {
        return sections.count
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return sections[section].items.count
    }
    
    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return sections[section].title
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: Keys.cell.rawValue, for: indexPath) as! ToDoCell
    
        let currentItem = sections[indexPath.section].items[indexPath.row]
        cell.titleLabel.text = currentItem.taskName
        cell.descriptionLabel.text = currentItem.description
        if currentItem.description?.isEmpty ?? false {
            cell.descLabelBottomConstraint.constant = 0
        } // you have to add an else clause to set the constraint to the default value
    
        cell.accessoryType = currentItem.isCompleted ? .checkmark : .none
        return cell
    }
    

It would be still more efficient to filter the items O(n) with a partition algorithm

let p = manager.tasks.partition(by: { $0.completed })
sections = [Section(title: "ToDo", items: Array(manager.tasks[p...])),
            Section(title: "Completed", items: Array(manager.tasks[..<p]))]
tableView.reloadData()
vadian
  • 274,689
  • 30
  • 353
  • 361
1

You want your original dataSource to be an array of the 2 different arrays (one with completed and one that is not completed.) [[]]

I found This one that seems pretty solid. However, it returns an dictionary, but i rewrote it slightly for you:

extension Sequence {
    func group<U: Hashable>(by key: (Iterator.Element) -> U) -> [[Iterator.Element]] {
        return Dictionary.init(grouping: self, by: key).map({$0.value})
    }
}

This way when you are in title header or cellForRowAt you can call it by manager.task[indexPath.section][indexPath.item]

Vollan
  • 1,887
  • 11
  • 26