UITableView
Cell IndexPath
iOS Development
Swift Programming
Table View Cell

How to get UITableViewCell indexPath from the Cell?

Interview Questions practice on Codemia

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

Browse interview questions

Introduction

Getting the IndexPath of a UITableViewCell is a common requirement when handling button taps, gesture recognizers, or other interactions inside custom table view cells. The standard approach is tableView.indexPath(for: cell), which returns an optional IndexPath. The cell reference typically comes from the sender of an action (a button inside the cell) by walking up the view hierarchy or using a delegate/closure pattern. This article covers all major approaches.

Using indexPath(for:)

swift
1// The standard UITableView method
2func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
3    let cell = tableView.cellForRow(at: indexPath)
4    print("Selected section \(indexPath.section), row \(indexPath.row)")
5}
6
7// Getting indexPath from a cell reference
8if let indexPath = tableView.indexPath(for: someCell) {
9    print("Cell is at row \(indexPath.row)")
10}

indexPath(for:) returns nil if the cell is not currently visible. This is the preferred API because it handles cell reuse correctly.

Button Inside a Cell (View Hierarchy)

swift
1class CustomCell: UITableViewCell {
2    @IBOutlet weak var actionButton: UIButton!
3}
4
5class ViewController: UITableViewController {
6
7    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
8        let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
9        cell.actionButton.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
10        return cell
11    }
12
13    @objc func buttonTapped(_ sender: UIButton) {
14        // Walk up the view hierarchy to find the cell
15        let point = sender.convert(CGPoint.zero, to: tableView)
16        guard let indexPath = tableView.indexPathForRow(at: point) else { return }
17
18        print("Button tapped in row \(indexPath.row)")
19        let item = dataSource[indexPath.row]
20        // Perform action with item
21    }
22}

convert(_:to:) translates the button's origin to table view coordinates. indexPathForRow(at:) returns the index path for the row at that point. This approach works regardless of how deeply nested the button is within the cell's view hierarchy.

Closure/Callback Pattern

swift
1class CustomCell: UITableViewCell {
2    var onButtonTapped: (() -> Void)?
3
4    @IBAction func buttonTapped(_ sender: UIButton) {
5        onButtonTapped?()
6    }
7}
8
9class ViewController: UITableViewController {
10
11    var items = ["Apple", "Banana", "Cherry"]
12
13    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
14        let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
15        cell.textLabel?.text = items[indexPath.row]
16
17        cell.onButtonTapped = { [weak self] in
18            guard let self = self else { return }
19            print("Tapped item: \(self.items[indexPath.row])")
20            self.deleteItem(at: indexPath)
21        }
22        return cell
23    }
24
25    func deleteItem(at indexPath: IndexPath) {
26        items.remove(at: indexPath.row)
27        tableView.deleteRows(at: [indexPath], with: .automatic)
28    }
29}

The closure captures the indexPath from cellForRowAt. This is clean and avoids view hierarchy traversal. However, the captured indexPath can become stale if rows are inserted or deleted without reloading.

Delegate Pattern

swift
1protocol CustomCellDelegate: AnyObject {
2    func customCell(_ cell: CustomCell, didTapButtonAt index: Int)
3}
4
5class CustomCell: UITableViewCell {
6    weak var delegate: CustomCellDelegate?
7    var buttonTag: Int = 0
8
9    @IBAction func buttonTapped(_ sender: UIButton) {
10        delegate?.customCell(self, didTapButtonAt: buttonTag)
11    }
12}
13
14class ViewController: UITableViewController, CustomCellDelegate {
15
16    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
17        let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
18        cell.delegate = self
19        cell.buttonTag = indexPath.row
20        return cell
21    }
22
23    func customCell(_ cell: CustomCell, didTapButtonAt index: Int) {
24        // Or use indexPath(for:) for the most reliable approach
25        if let indexPath = tableView.indexPath(for: cell) {
26            print("Delegate: row \(indexPath.row)")
27        }
28    }
29}

The delegate pattern passes the cell itself back to the controller, where indexPath(for:) gets the current index path. This avoids stale index path issues.

Using Tag Property

swift
1override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
2    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
3    cell.tag = indexPath.row  // Simple but fragile
4
5    let button = cell.viewWithTag(100) as? UIButton
6    button?.tag = indexPath.row
7    button?.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
8    return cell
9}
10
11@objc func buttonTapped(_ sender: UIButton) {
12    let row = sender.tag
13    print("Row: \(row)")
14}

Using tag is simple but fragile. Tags become incorrect after row insertions, deletions, or reordering unless the table is reloaded. Prefer indexPath(for:) or the closure pattern.

Common Pitfalls

  • Cell not visible returns nil: indexPath(for:) returns nil for cells that have scrolled off-screen and been recycled. Always handle the optional safely with guard let or if let.
  • Stale captured indexPath: Closures capturing indexPath from cellForRowAt become invalid after insertions or deletions. Use indexPath(for: cell) inside the closure for the current position.
  • Walking superview chain directly: Code like cell.superview?.superview as? UITableView breaks across iOS versions because the view hierarchy changes. Use convert(_:to:) with indexPathForRow(at:) instead.
  • Using tag for row identification: cell.tag = indexPath.row breaks after row operations (insert, delete, move) unless you reload the entire table. It also conflicts with other uses of the tag property.
  • Adding targets multiple times: addTarget in cellForRowAt adds a new target each time the cell is reused. Either remove the previous target first or configure the target once in the cell's awakeFromNib.

Summary

  • Use tableView.indexPath(for: cell) as the primary method — it returns the current index path
  • For buttons inside cells, use convert(CGPoint.zero, to: tableView) + indexPathForRow(at:)
  • The closure pattern is concise but capture [weak self] and re-fetch the index path if rows change
  • The delegate pattern is the most robust for complex cells with multiple interactive elements
  • Avoid tag-based approaches — they break silently when rows are inserted, deleted, or reordered

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.