UITableViewCell
button click
iOS development
Swift programming
mobile app development

Get button click inside UITableViewCell

Interview Questions practice on Codemia

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

Browse interview questions

Introduction

Handling a button tap inside a UITableViewCell is a small problem that exposes a bigger UIKit rule: cells are reusable views, not the source of truth for application state. The clean solution is to let the cell report the tap and let the table view controller decide what that tap means for the underlying data.

A Reliable Pattern

The simplest modern pattern is to give the cell a callback or delegate. The cell handles the button press locally, and the controller supplies the action when configuring the cell.

swift
1import UIKit
2
3struct TaskItem {
4    let id: UUID
5    let title: String
6}
7
8final class TaskCell: UITableViewCell {
9    static let reuseIdentifier = "TaskCell"
10
11    private let actionButton = UIButton(type: .system)
12    var onButtonTap: (() -> Void)?
13
14    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
15        super.init(style: style, reuseIdentifier: reuseIdentifier)
16
17        actionButton.setTitle("Done", for: .normal)
18        actionButton.addTarget(self, action: #selector(handleButtonTap), for: .touchUpInside)
19        accessoryView = actionButton
20    }
21
22    required init?(coder: NSCoder) {
23        fatalError("init(coder:) has not been implemented")
24    }
25
26    override func prepareForReuse() {
27        super.prepareForReuse()
28        onButtonTap = nil
29    }
30
31    func configure(with item: TaskItem) {
32        textLabel?.text = item.title
33    }
34
35    @objc private func handleButtonTap() {
36        onButtonTap?()
37    }
38}

This keeps the cell reusable and focused. It knows that a button was tapped, but it does not try to mutate the data source on its own.

Wiring the Tap Back to the Controller

The table view controller configures the cell and decides what to do with the tap. A useful trick is to capture a stable model identifier instead of the current row number, because rows can move when you insert, delete, or sort.

swift
1import UIKit
2
3final class TasksViewController: UITableViewController {
4    private var items: [TaskItem] = [
5        TaskItem(id: UUID(), title: "Buy milk"),
6        TaskItem(id: UUID(), title: "Ship release"),
7        TaskItem(id: UUID(), title: "Reply to review")
8    ]
9
10    override func viewDidLoad() {
11        super.viewDidLoad()
12        tableView.register(TaskCell.self, forCellReuseIdentifier: TaskCell.reuseIdentifier)
13    }
14
15    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
16        items.count
17    }
18
19    override func tableView(
20        _ tableView: UITableView,
21        cellForRowAt indexPath: IndexPath
22    ) -> UITableViewCell {
23        guard let cell = tableView.dequeueReusableCell(
24            withIdentifier: TaskCell.reuseIdentifier,
25            for: indexPath
26        ) as? TaskCell else {
27            return UITableViewCell()
28        }
29
30        let item = items[indexPath.row]
31        cell.configure(with: item)
32        cell.onButtonTap = { [weak self] in
33            self?.markTaskDone(id: item.id)
34        }
35        return cell
36    }
37
38    private func markTaskDone(id: UUID) {
39        guard let index = items.firstIndex(where: { $0.id == id }) else { return }
40        print("Tapped row for:", items[index].title)
41    }
42}

This is easier to maintain than making the cell climb the view hierarchy or guess its own index path.

Delegate Pattern Versus Closure Pattern

Closures are compact and work well when the cell only has one or two actions. A delegate protocol is better when the cell needs to report several events or when your team prefers a more explicit contract.

The design rule is the same either way:

  • the cell owns the button
  • the controller owns the data
  • the event travels from the cell back to the controller

Once you preserve that direction, reuse bugs become much less common.

Why button.tag Is Fragile

A common shortcut is setting button.tag = indexPath.row and reading that tag later. It can appear to work, but it breaks easily when rows are inserted, deleted, filtered, or reordered. The tag is just an integer snapshot from configuration time, not a durable connection to the current model.

Another fragile approach is walking up superview references until you find a cell. That depends on UIKit view hierarchy details you do not control. It also makes the code harder to read than a direct callback.

If You Need the Current Index Path

Sometimes the action truly depends on the cell's current table position. In that case, ask the table view for the index path when the tap arrives:

swift
1extension UIView {
2    func superview<T: UIView>(of type: T.Type) -> T? {
3        var current = self.superview
4        while let view = current {
5            if let typed = view as? T { return typed }
6            current = view.superview
7        }
8        return nil
9    }
10}
11
12if let cell = sender.superview(of: TaskCell.self),
13   let indexPath = tableView.indexPath(for: cell) {
14    print(indexPath.row)
15}

That pattern is still secondary to model identifiers, but it is safer than storing a row number in a tag and assuming it will remain correct forever.

Common Pitfalls

  • Using button.tag as if it were a stable model identifier.
  • Letting the cell update controller-owned data directly.
  • Forgetting to clear callbacks in prepareForReuse.
  • Creating a retain cycle by capturing the controller strongly inside the callback.
  • Walking the superview chain instead of passing the event back cleanly.

Summary

  • The clean solution is to let the cell report the tap and let the controller handle the data change.
  • Closures are a compact option; delegates are a good fit for more complex cells.
  • Prefer model identifiers over row numbers because table rows can move.
  • Reuse matters, so reset callbacks in prepareForReuse.
  • Avoid tags and superview-walking when a direct callback pattern is available.

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.