tableView.indexPathForRow(at: to:) returning an index that is off by one.

Hi guys,

I don't know if it's something in my code or a bug but for some reason on my latest project tableView.indexPathForRow(at: to:) is returning an index that is off by one.

I have a stripped down tableview controller with a custom cell and button linked to the "testButton" method. I also tagged the button with the indexPath.row from the cellForRowAt method so I know the cell is in the correct place.

The problem is, in tableView.indexPathForRow, the tag shows up with the expected value, but the indexPath.row from that method is off by one.

class TestListViewController: UITableViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        configureTableView()
    }
}

extension TestListViewController {
    private func configureTableView() {
        tableView.register(TestCell.self, forCellReuseIdentifier: "TestCell")
    }

@objc func testButton(_ sender: UIButton) {
        guard let indexPath = tableView.indexPathForRow(at: sender.convert(CGPoint.zero, to: self.tableView)) else {
            return
        }
        print("Tag: \(sender.tag)") //This comes up as expected and the tag matches the row
        print("IndexPath: \(indexPath)")    //This does not, index row is off by one! 
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "TestCell", for: indexPath) as! TestCell
        cell.nameLabel.text = "Row \(indexPath.row)"
        cell.checkbox.addTarget(self, action: #selector(testButton(_ :)), for: .touchUpInside)
        cell.checkbox.tag = indexPath.row
        return cell
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 50
    }
    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
}

In my actual app there is a checkbox on each cell and if I tap the first cells checkbox obviously the app crashes because the index is out of range, if I tap the second cells checkbox, the first cells checkbox fills in.

I'm at an utter loss, I have used this method many times in production apps just fine so I'm hoping it's just something I'm forgetting to do.

Post not yet marked as solved Up vote post of SN81 Down vote post of SN81
579 views
  • indexPathForRow uses a CGPoint to determine the corresponding cell indexPath. You are using sender.convert(CGPoint.zero, to: self.tableView) where sender is your button, which corresponds to the top left point of your button in tableView coordinates. Are you sure this point is in the apporpriate cell you want it to be? Also: The CGPoint you use for indexPathForRow needs to be in the contentView of the appropriate cell, so for example a point with negative x value won't work.

Add a Comment

Replies

As there has been no activity on this thread I post my original comment as a reply as I am pretty sure this is the solution to the problem of the thread starter:

indexPathForRow uses a CGPoint to determine the corresponding cell indexPath. The point you are using sender.convert(CGPoint.zero, to: self.tableView) where sender is your button, which corresponds to the top left point of your button in tableView coordinates. This point probably is not in the cell you want it to be. So make sure your button has the appropriate frame or transform the point you use to be in the appropriate cell after transformation (maybe you want to project it onto the cell's contentView frame).

Also: The CGPoint you use for indexPathForRow needs to be in the contentView of the appropriate cell, so for example a point with negative x value won't work. Again, projecting the point onto the appropriate cell's contentView frame should help in that case.

I miss something.

In

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

you do not recreate the checkbox, just its tag. So you reuse a checkbox from the dequeued cell, which may be the previous checkbox. You just change the target, not the frame.

To check, print sender.frame in testButton()

I would try to change as follows:

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "TestCell", for: indexPath) as! TestCell
        cell.nameLabel.text = "Row \(indexPath.row)"
        cell.checkbox = // <<-- create the checkbox or set its frame
        cell.checkbox.addTarget(self, action: #selector(testButton(_ :)), for: .touchUpInside)
        cell.checkbox.tag = indexPath.row
        return cell
    }