Home > Back-end >  Facing issue in selecting and deselecting tableview cell in swift
Facing issue in selecting and deselecting tableview cell in swift

Time:12-11

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 = 0
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
}


func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
   
    self.pincodes[indexPath.row].isSelected = !self.pincodes[indexPath.row].isSelected
    pincodes[pincodePreviousIndex].isSelected = false
    
    if self.pincodes[indexPath.row].isSelected == true {
    }else {
    }
    pincodePreviousIndex = indexPath.row
}
}

CodePudding user response:

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
}

CodePudding user response:

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.

  • Related