UITableViewCell unintentionally placing checkmark on unwanted rows

I am making a music genre picking application and when I go to my table to select genres, I select a row and it selects a random row about 10 or so down from my selection.

My code for the selection is:

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let genresFromLibrary = genrequary.collections
    let rowitem = genresFromLibrary![indexPath.row].representativeItem
    print(rowitem?.value(forProperty: MPMediaItemPropertyGenre) as! String
    )
    if let cell = tableView.cellForRow(at: indexPath)
    {
        cell.accessoryType = .checkmark
    }
}

override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
    if let cell = tableView.cellForRow(at: indexPath)
    {
        cell.accessoryType = .none
    }
}

2 answers

  • answered 2018-02-13 02:19 Andy Ibanez

    Cells are reused by default when cellForRowAtIndexPath is called. This causes the cells to have the wrong data when you don't keep track of the indexPaths that have been selected. You need to keep track of the index paths that are currently selected so you can show the appropriate accessory type in your table view.

    One way of doing it is to have a property in your UITableViewController that just stores the index paths of the selected cells. It can be an array or a set.

    var selectedIndexPaths = Set<IndexPath>()
    

    When you select a row on didSelectRowAt, add or remove the cell from selectedIndexPaths, depending on whether the index path is already in the array or not:

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        if selectedIndexPaths.contains(indexPath) {
             // The index path is already in the array, so remove it.
             selectedIndexPaths.remove(indexPathIndex)
        } else {
             // The index path is not part of the array
             selectedIndexPaths.append(indexPath)
        }
    
        // Show the changes in the selected cell (otherwise you wouldn't see the checkmark or lack thereof until cellForRowAt got called again for this cell).
        tableView.reloadRows(at: [indexPath], with: .none)
    }
    

    Once you have this, on your cellForRowAtIndexPath, check if the indexPath is in the selectedIndexPaths array to choose the accessoryType.

    if selectedIndexPaths.contains(indexPath) {
        // Cell is selected
        cell.accessoryType = .checkmark
    } else {
        cell.accessoryType = .none
    }
    

    This should solve the problem of the seemingly random cells that are checked every 10 cells down or so (which, is not random, it's just that the cell with the checkmark is being reused).

  • answered 2018-02-13 02:19 AntiMoron

    Because cellForRow returns a cached cell you generated. When scrolling out of the screen the order of cells are changed and cells are reused. So it seems "randomly selected".

    Don use cellForRow, instead record selection data.

    Here's code works in a single view playground.

    import UIKit
    import PlaygroundSupport
    
    class MyViewController : UIViewController, UITableViewDataSource, UITableViewDelegate {
    
        let tableView = UITableView()
        var selection: [IndexPath: Bool] = [:]
    
        override func viewDidLoad() {
            super.viewDidLoad()
            tableView.delegate = self
            tableView.dataSource = self
            tableView.tableFooterView = UIView()
            view.addSubview(tableView)
        }
    
        override func viewDidLayoutSubviews() {
            super.viewDidLayoutSubviews()
            tableView.frame = self.view.bounds
        }
    
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: "c")
            if let sc = cell {
                sc.accessoryType = .none
                let isSelected = selection[indexPath] ?? false
                sc.accessoryType = isSelected ? .checkmark : .none
                return sc
            }
            return UITableViewCell(style: .default, reuseIdentifier: "c")
        }
    
        func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
            cell.textLabel?.text = NSNumber(value: indexPath.row).stringValue
        }
    
        func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
            selection[indexPath] = true
            tableView.cellForRow(at: indexPath)?.accessoryType = .checkmark
        }
    
        func numberOfSections(in tableView: UITableView) -> Int {
            return 1
        }
    
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return 30
        }
    
    }
    // Present the view controller in the Live View window
    PlaygroundPage.current.liveView = MyViewController()