iOS development
UITableView
Swift programming
iOS UI
UITableView cell interaction

Getting row of UITableView cell on button press

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

When a button lives inside a UITableViewCell, the button action does not automatically tell you which row triggered it. The clean solution is to map that tap back to an IndexPath, either by asking the table view for the touched cell location or, even better, by wiring the cell to report actions through a callback.

Quick Approach: Convert the Button Position to an IndexPath

If you already have a button target action and want the row immediately, convert a point from the button into the table view's coordinate space.

swift
1import UIKit
2
3final class ViewController: UIViewController, UITableViewDataSource {
4    @IBOutlet private weak var tableView: UITableView!
5    private let items = ["One", "Two", "Three"]
6
7    @IBAction private func didTapButton(_ sender: UIButton) {
8        let point = sender.convert(CGPoint.zero, to: tableView)
9        guard let indexPath = tableView.indexPathForRow(at: point) else { return }
10        print("Tapped row:", indexPath.row)
11    }
12
13    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
14        items.count
15    }
16
17    func tableView(
18        _ tableView: UITableView,
19        cellForRowAt indexPath: IndexPath
20    ) -> UITableViewCell {
21        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
22        cell.textLabel?.text = items[indexPath.row]
23        return cell
24    }
25}

This works well for simple screens. The key step is indexPathForRow(at:), not reading a stored row number from the button itself.

Better Design: Let the Cell Report Its Own Action

For maintainable code, avoid making the view controller search the hierarchy every time. A cleaner pattern is to let the cell expose a closure or delegate, then ask the table view for the cell's index path when the callback fires.

swift
1import UIKit
2
3final class ActionCell: UITableViewCell {
4    var onButtonTap: (() -> Void)?
5
6    @IBAction private func didTapActionButton(_ sender: UIButton) {
7        onButtonTap?()
8    }
9}
swift
1import UIKit
2
3final class ViewController: UIViewController, UITableViewDataSource {
4    @IBOutlet private weak var tableView: UITableView!
5    private let items = ["One", "Two", "Three"]
6
7    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
8        items.count
9    }
10
11    func tableView(
12        _ tableView: UITableView,
13        cellForRowAt indexPath: IndexPath
14    ) -> UITableViewCell {
15        let cell = tableView.dequeueReusableCell(withIdentifier: "ActionCell", for: indexPath) as! ActionCell
16        cell.textLabel?.text = items[indexPath.row]
17        cell.onButtonTap = { [weak self, weak cell] in
18            guard
19                let self = self,
20                let cell = cell,
21                let tappedIndexPath = self.tableView.indexPath(for: cell)
22            else { return }
23
24            print("Tapped row:", tappedIndexPath.row)
25        }
26        return cell
27    }
28}

This scales better because the cell owns the button interaction and the controller owns the data lookup.

Why Button Tags Are Fragile

You will often see code like:

swift
button.tag = indexPath.row

That works in small demos, but it becomes fragile when rows move, sections are added, or data is reloaded asynchronously. Tags are just integers with no table-view semantics, so they are easy to desynchronize from the actual cell state.

If the table can reorder, insert, or delete rows, deriving the IndexPath at tap time is much safer.

Keep the Real Model ID Nearby

Even when you know the row, what you often really want is the underlying item identifier. In practice, the best architecture is usually:

  • map the tap to an IndexPath
  • use the IndexPath to look up the model
  • act on the model's stable identifier

That prevents UI position from becoming the business identifier accidentally.

Common Pitfalls

  • Storing indexPath.row in button.tag and assuming it will stay correct after reloads or inserts.
  • Searching the superview chain manually to find the cell, which is brittle against layout changes.
  • Ignoring sections and using only row when the table has multiple sections.
  • Capturing the wrong cell in a reused table-view cell without updating callbacks during configuration.
  • Treating the visible row as the real data identity instead of mapping back to the model object.

Summary

  • The direct fix is to convert the button position and ask the table view for the matching IndexPath.
  • A cleaner long-term pattern is a cell callback combined with tableView.indexPath(for:).
  • Avoid relying on button.tag for anything beyond tiny demos.
  • Use the row to find the model, not as the final business identifier.
  • Table-view cell reuse makes explicit configuration and callback wiring important.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.