skip to Main Content

I am showing pincodes in tableview, and when i select a cell then it should select and if i tap on the same cell again then it should deselect(while tapping cell should work like switch)

enter image description here

but with below code

issue 1: initially i am unable to select 1st row but after selecting any other row and then able to select 1st row.. why? where am i wrong?

issue 2: only one time i can select deselect the same row with two tapping if i tap 3rd time continuously then unable to select the same row, why?.. please guide

class PincodeModel{
var name: String?
var id: Int?
var isSelected: Bool

init(name: String?, id: Int?, isSelected: Bool) {
    self.name = name
    self.id = id
    self.isSelected = isSelected
}
}


class FilterViewController: UIViewController {

var pincodePreviousIndex: Int = -1
var pincodes = [PincodeModel]()

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    
    for pincode in pincodeList {
        self.pincodes.append(PincodeModel(name: pincode, id: 0, isSelected: false))
    }
}


func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let cell = tableView.dequeueReusableCell(withIdentifier: "SubFilterTableViewCell", for: indexPath) as! SubFilterTableViewCell
        cell.title.text = self.pincodes[indexPath.row].name

        if !self.pincodes.isEmpty {
            if self.pincodes[indexPath.row].isSelected == true {
                cell.tickImageView.image =  #imageLiteral(resourceName: "iconTick")
            }else {
                cell.tickImageView.image = UIImage()
            }
        }
    return cell
}

 // EDITED Code according to below answer
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
   
    self.pincodes[indexPath.row].isSelected = !self.pincodes[indexPath.row].isSelected

if self.pincodes[indexPath.row].isSelected == true {
self.filterData.pincode = pincodes[indexPath.row].name ?? ""
}else {
self.filterData.pincode = ""
}
if pincodePreviousIndex > 0 && pincodePreviousIndex != indexPath.row {
pincodes[pincodePreviousIndex].isSelected = false
}
pincodePreviousIndex = indexPath.row

}

this is working as i want when i select from index = 1, but if i select first row(index = 0) then the right mark remains if i select another row, why?

o/p with edited code:

enter image description here

2

Answers


  1. For issue 1 – By using this line of code:

    var pincodePreviousIndex: Int = 0

    You cannot click the first row until you click another since

    pincodes[pincodePreviousIndex].isSelected = false

    Since you’re defaulting to 0 in the beginning, that correlates to the first row.

    For issue 2 – if you select row 2 (selected) and then select it again to deselect it: pincodePreviousIndex will hold the value of that row and then deselect it again with

    pincodes[pincodePreviousIndex].isSelected = false

    So even though you’re selecting it it will deselect it.

    I would do this at the top:
    var pincodePreviousIndex: Int = -1

    and at the bottom:

    if pincodePreviousIndex > 0 && pincodePreviousIndex != indexPath.row {
        pincodes[pincodePreviousIndex].isSelected = false
    }
    
    Login or Signup to reply.
  2. There are a couple approaches you can take to save yourself some trouble.

    First, set .selectionStyle = .none on your cells, and then in your cell class, override setSelected(...). For example, I added an image view to my cell and gave it an empty-box as its image, and a checked-box as its highlighted image:

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
        imgView.isHighlighted = selected ? true : false
    }
    

    Now the cell appearance will reflect its selected state which is maintained by the table view.

    Next, instead of didSelectRowAt, we’ll implement willSelectRowAt … if the cell is currently selected, we’ll deselect it (and update our data):

    func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
        // is a row already selected?
        if let idx = tableView.indexPathForSelectedRow {
            if idx == indexPath {
                // tapped row is already selected, so
                //  deselect it
                tableView.deselectRow(at: indexPath, animated: false)
                //  update our data
                pincodes[indexPath.row].isSelected = false
                //  tell table view NOT to select the row
                return nil
            } else {
                // some other row is selected, so
                //  update our data
                //  table view will automatically deselect that row
                pincodes[idx.row].isSelected = false
            }
        }
        // tapped row should now be selected, so
        //  update our data
        pincodes[indexPath.row].isSelected = true
        //  tell table view TO select the row
        return indexPath
    }
    

    Here’s a complete example:

    class PincodeModel{
        var name: String?
        var id: Int?
        var isSelected: Bool
        
        init(name: String?, id: Int?, isSelected: Bool) {
            self.name = name
            self.id = id
            self.isSelected = isSelected
        }
    }
    
    class SelMethodTableViewController: UIViewController {
    
        var pincodes: [PincodeModel] = []
        
        let tableView = UITableView()
        
        let infoView: UIView = {
            let v = UILabel()
            v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
            return v
        }()
        let infoTitle: UILabel = {
            let v = UILabel()
            v.text = "Info:"
            return v
        }()
        let infoLabel: UILabel = {
            let v = UILabel()
            v.numberOfLines = 0
            return v
        }()
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            for i in 0..<20 {
                let pcm = PincodeModel(name: "(i)", id: i, isSelected: false)
                pincodes.append(pcm)
            }
    
            [tableView, infoView, infoTitle, infoLabel].forEach { v in
                v.translatesAutoresizingMaskIntoConstraints = false
            }
            [infoTitle, infoLabel].forEach { v in
                infoView.addSubview(v)
            }
            [tableView, infoView].forEach { v in
                view.addSubview(v)
            }
            
            let g = view.safeAreaLayoutGuide
            
            NSLayoutConstraint.activate([
                
                // constrain the table view on right-side of view
                tableView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
                tableView.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.5),
                tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                tableView.bottomAnchor.constraint(equalTo: infoView.topAnchor, constant: -16.0),
    
                // let's add a tappable "info" view below the table view
                infoView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                infoView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                infoView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
                infoView.heightAnchor.constraint(equalToConstant: 120.0),
                
                // add labels to infoView
                infoTitle.topAnchor.constraint(equalTo: infoView.topAnchor, constant: 8.0),
                infoTitle.leadingAnchor.constraint(equalTo: infoView.leadingAnchor, constant: 8.0),
                infoTitle.trailingAnchor.constraint(equalTo: infoView.trailingAnchor, constant: -8.0),
                infoLabel.topAnchor.constraint(equalTo: infoTitle.bottomAnchor, constant: 8.0),
                infoLabel.leadingAnchor.constraint(equalTo: infoView.leadingAnchor, constant: 8.0),
                infoLabel.trailingAnchor.constraint(equalTo: infoView.trailingAnchor, constant: -8.0),
                //infoLabel.bottomAnchor.constraint(lessThanOrEqualTo: infoView.bottomAnchor, constant: -8.0),
                
            ])
    
            tableView.dataSource = self
            tableView.delegate = self
            
            tableView.register(MyToggleCell.self, forCellReuseIdentifier: "toggleCell")
            
            // just so we can see the frame of the table view
            tableView.layer.borderWidth = 1.0
            tableView.layer.borderColor = UIColor.red.cgColor
    
            let t = UITapGestureRecognizer(target: self, action: #selector(showInfo(_:)))
            infoView.addGestureRecognizer(t)
            infoView.isUserInteractionEnabled = true
        }
    
        @objc func showInfo(_ g: UIGestureRecognizer) -> Void {
            var s: String = ""
            
            let selectedFromData = pincodes.filter( {$0.isSelected == true} )
    
            s += "Data reports:"
            if selectedFromData.count > 0 {
                selectedFromData.forEach { ob in
                    let obID = ob.id ?? -1
                    s += " (obID)"
                }
            } else {
                s += " Nothing selected"
            }
            s += "n"
            s += "Table reports: "
            if let selectedFromTable = tableView.indexPathsForSelectedRows {
                selectedFromTable.forEach { idx in
                    s += " (idx.row)"
                }
            } else {
                s += " No rows selected"
            }
            infoLabel.text = s
        }
    }
    
    extension SelMethodTableViewController: UITableViewDataSource, UITableViewDelegate {
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return pincodes.count
        }
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let c = tableView.dequeueReusableCell(withIdentifier: "toggleCell", for: indexPath) as! MyToggleCell
            c.label.text = pincodes[indexPath.row].name
            c.selectionStyle = .none
            return c
        }
        func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
            // is a row already selected?
            if let idx = tableView.indexPathForSelectedRow {
                if idx == indexPath {
                    // tapped row is already selected, so
                    //  deselect it
                    tableView.deselectRow(at: indexPath, animated: false)
                    //  update our data
                    pincodes[indexPath.row].isSelected = false
                    //  tell table view NOT to select the row
                    return nil
                } else {
                    // some other row is selected, so
                    //  update our data
                    //  table view will automatically deselect that row
                    pincodes[idx.row].isSelected = false
                }
            }
            // tapped row should now be selected, so
            //  update our data
            pincodes[indexPath.row].isSelected = true
            //  tell table view TO select the row
            return indexPath
        }
    }
    
    class MyToggleCell: UITableViewCell {
        let imgView: UIImageView = {
            let v = UIImageView()
            return v
        }()
        let label: UILabel = {
            let v = UILabel()
            return v
        }()
        
        override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
            super.init(style: style, reuseIdentifier: reuseIdentifier)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        func commonInit() -> Void {
            [imgView, label].forEach { v in
                v.translatesAutoresizingMaskIntoConstraints = false
                contentView.addSubview(v)
            }
            let g = contentView.layoutMarginsGuide
            
            // give bottom anchor less-than-required
            //  to avoid auto-layout complaints
            let b = imgView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -4.0)
            b.priority = .required - 1
            
            NSLayoutConstraint.activate([
                imgView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
                imgView.topAnchor.constraint(equalTo: g.topAnchor, constant: 4.0),
                imgView.widthAnchor.constraint(equalToConstant: 32.0),
                imgView.heightAnchor.constraint(equalTo: imgView.widthAnchor),
                b,
                
                label.centerYAnchor.constraint(equalTo: g.centerYAnchor, constant: 0.0),
                label.leadingAnchor.constraint(equalTo: imgView.trailingAnchor, constant: 16.0),
                label.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
            ])
            if let img1 = UIImage(systemName: "square"),
               let img2 = UIImage(systemName: "checkmark.square") {
                imgView.image = img1
                imgView.highlightedImage = img2
            }
        }
    
        override func setSelected(_ selected: Bool, animated: Bool) {
            super.setSelected(selected, animated: animated)
            imgView.isHighlighted = selected ? true : false
        }
    }
    

    It will look like this:

    enter image description here

    When running:

    • Tapping a row will select that row
    • Tapping a different row will select the new row and deselect the currently selected row
    • Tapping the already-selected row will deselect it
    • Tapping the gray "info view" will report on the selection states from both the data and the table view

    enter image description here

    enter image description here

    Note that if a selected row is scrolled out-of-view, it will remain selected (and will show selected when scrolled back into view) and the data and table view selection states will continue to be correct.


    Edit

    If we want to use didSelectRowAt (perhaps for other uses), we can "toggle" the selected row like this:

    func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
        
        // if the tapped row is already selected
        if let indexPathForSelectedRow = tableView.indexPathForSelectedRow,
           indexPathForSelectedRow == indexPath {
            tableView.deselectRow(at: indexPath, animated: false)
            // calling .deselectRow(at: ...) does NOT trigger a call to didDeselectRowAt
            // so update our data here
            pincodes[indexPath.row].isSelected = false
            return nil
        }
        return indexPath
        
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print("did select", indexPath)
        pincodes[indexPath.row].isSelected = true
    }
    func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
        print("did deselect", indexPath)
        pincodes[indexPath.row].isSelected = false
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search