Luke Ireton
Luke Ireton

Reputation: 509

UITableView Custom Cell button selects other buttons out of the main view?

On my custom tableviewcell I have a button which the user can press which toggles between selected and unselected. This is just a simple UIButton, this button is contained with my customtableviewcell as an outlet.

import UIKit

class CustomCellAssessment: UITableViewCell {

@IBOutlet weak var name: UILabel!
@IBOutlet weak var dateTaken: UILabel!
@IBOutlet weak var id: UILabel!
@IBOutlet weak var score: UILabel!
@IBOutlet weak var button: UIButton!


override func awakeFromNib() {
     super.awakeFromNib()
    // Initialization code
}

 override func setSelected(_ selected: Bool, animated: Bool) {
    super.setSelected(selected, animated: animated)


    // Configure the view for the selected state
}

@IBAction func selectButton(_ sender: UIButton) {

if sender.isSelected{

  sender.isSelected = false

}else{
  sender.isSelected = true
} 
} 
}

The strange thing is, when I press a button on say the first cell it then selects a button 8 cells down on another cell (out of the view) in the same tableview. Each cell has its own button but it is as if using dequeReusableCell is causing the system to behave this way. Why is it doing this and is it to do with the way that UIbuttons work on tableviewcells?

Thanks

Upvotes: 0

Views: 1470

Answers (4)

crom87
crom87

Reputation: 1381

Each cell has its own button but it is as if using dequeReusableCell is causing the system to behave this way.

Wrong. UITableViewCells are reusable, so if your tableView has 8 cells visible, when loading the 9th, the cell nr 1 will be reused.

Solution: You need to keep track of the state of your cells. When the method cellForRowAtIndexPath is called, you need to configure the cell from scratch.

You could have in your ViewController an small array containing the state of the cells:

var cellsState: [CellState]

and store there the selected state for each indexPath. Then in the cellForRowAtIndexPath, you configure the cell with the state.

cell.selected = self.cellsState[indexPath.row].selected

So, an overview of I would do is:

1 - On the cellForRowAtIndexPath, I would set

cell.button.tag = indexPath.row
cell.selected = cellsState[indexPath.row].selected

2 - Move the IBAction to your ViewController or TableViewController

3 - When the click method is called, update the selected state of the cell

self.cellsState[sender.tag].selected = true

Remember to always configure the whole cell at cellForRowAtIndexPath

Edit:

import UIKit

struct CellState {
    var selected:Bool
    init(){
        selected = false
    }
}

class MyCell: UITableViewCell {

    @IBOutlet weak var button: UIButton!

    override func awakeFromNib() {
        self.button.setTitleColor(.red, for: .selected)
        self.button.setTitleColor(.black, for: .normal)
    }

}

class ViewController: UIViewController, UITableViewDataSource {

    var cellsState:[CellState] = []

    @IBOutlet weak var tableView: UITableView!


    override func viewDidLoad() {
        super.viewDidLoad()

        //Add state for 5 cells.
        for _ in 0...5 {
            self.cellsState.append(CellState())
        }
    }   


    @IBAction func didClick(_ sender: UIButton) {

        self.cellsState[sender.tag].selected = true
        self.tableView.reloadData()
    }

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

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! MyCell
        cell.button.isSelected = self.cellsState[indexPath.row].selected
        cell.button.tag = indexPath.row
        return cell
    }
}

Upvotes: 1

kathayatnk
kathayatnk

Reputation: 1015

One way to approach your problem is as follow

1. First create a protocol to know when button is clicked

protocol MyCellDelegate: class {
    func cellButtonClicked(_ indexPath: IndexPath)
}

2. Now in your cell class, you could do something like following

class MyCell: UITableViewCell {

var indexPath: IndexPath?
weak var cellButtonDelegate: MyCellDelegate?


func configureCell(with value: String, atIndexPath indexPath: IndexPath, selected: [IndexPath]) {
        self.indexPath = indexPath  //set the indexPath
        self.textLabel?.text = value
        if selected.contains(indexPath) {
            //this one is selected so do the stuff
            //here we will chnage only the background color
            backgroundColor = .red
            self.textLabel?.textColor = .white

        } else {
            //unselected
            backgroundColor = .white
            self.textLabel?.textColor = .red
        }
}

    @IBAction func buttonClicked(_ sender: UIButton) {
        guard let delegate = cellButtonDelegate, let indexPath = indexPath else { return }
        delegate.cellButtonClicked(indexPath)
    }
}

3. Now in your controller. I'm using UItableViewController here

class TheTableViewController: UITableViewController, MyCellDelegate {

    let cellData = ["cell1","cell2","cell3","cell4","cell2","cell3","cell4","cell2","cell3","cell4","cell2","cell3","cell4","cell2","cell3","cell4","cell2","cell3","cell4"]
    var selectedIndexpaths = [IndexPath]()


    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(MyCell.self, forCellReuseIdentifier: "MyCell")
    }


    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return cellData.count
    }

    override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 55.0
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath) as! MyCell
        cell.cellButtonDelegate = self
        cell.configureCell(with: cellData[indexPath.row], atIndexPath: indexPath, selected: selectedIndexpaths)
        return cell
    }

    override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return 30.0
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let cell = tableView.cellForRow(at: indexPath) as! MyCell
        cell.buttonClicked(UIButton())
    }

    func cellButtonClicked(_ indexPath: IndexPath) {
        if selectedIndexpaths.contains(indexPath) {
            //this means cell has already selected state
            //now we will toggle the state here from selected to unselected by removing indexPath
            if let index = selectedIndexpaths.index(of: indexPath) {
                selectedIndexpaths.remove(at: index)
            }

        } else {
            //this is new selection so add it
            selectedIndexpaths.append(indexPath)
        }

        tableView.reloadData()
    }
}

Upvotes: 0

sazid008
sazid008

Reputation: 185

In your tableView(_: cellForRowAt:) take action for button click and add target

cell.button.addTarget(self, action: #selector(self.selectedButton), for: .touchUpInside)

At the target method use below lines to get indexPath

func selectedButton(sender: UIButton){
       let hitPoint: CGPoint = sender.convert(CGPoint.zero, to: self.tableView)
       let indexPath: NSIndexPath = self.tableView.indexPathForRow(at: hitPoint)! as NSIndexPath
}

Then do your stuff by using that indexPath. Actually your method can not find in which indexPath button is clicked that's why not working your desired button.

Upvotes: 0

jrturton
jrturton

Reputation: 119292

When you tap the button, you're setting the selected state of the button. When the cell gets reused, the button is still selected - you're not doing anything to reset or update that state.

If button selection is separate to table cell selection, then you'll need to keep track of the index path(s) of the selected buttons as part of your data model and then update the selected state of the button in tableView(_: cellForRowAt:).

A table view will only create as many cells as it needs to display a screen-and-a-bits worth of information. After that, cells are reused. If a cell scrolls off the top of the screen, the table view puts it in a reuse queue, then when it needs to display a new row at the bottom as you scroll, it pulls that out of the queue and gives it to you in tableView(_: cellForRowAt:).

The cell you get here will be exactly the same as the one you used 8 or so rows earlier, and it's up to you to configure it completely. You can do that in cellForRow, and you also have an opportunity in the cell itself by implementing prepareForReuse, which is called just before the cell is dequeued.

Upvotes: 1

Related Questions