UITableView
iOS Development
Custom AccessoryView
UITableViewDelegate
Swift Programming

Using a custom image for a UITableViewCell's accessoryView and having it respond to UITableViewDelegate

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

A UITableViewCell can show a custom accessory view instead of the built-in disclosure indicator or detail button. The important detail is that a custom accessoryView is just a normal UIView; it does not automatically route taps through the same delegate callback used by the built-in accessory button types.

Why accessoryView Does Not Trigger the Usual Delegate Method

Many developers expect tableView(_:accessoryButtonTappedForRowWith:) to fire whenever the accessory area is tapped. That is only true for the standard accessory button types, such as .detailDisclosureButton. If you assign your own view to cell.accessoryView, UIKit does not treat it as that built-in control.

So there are really two different behaviors:

  • built-in accessory button types participate in the dedicated delegate callback
  • custom accessory views must handle interaction themselves

That is why setting a plain UIImageView as the accessory view usually does nothing when tapped. UIImageView is not a control, and even if you enable user interaction, UIKit still will not magically map the tap into the table view delegate method.

Use a UIButton as the Custom Accessory View

The practical fix is to use a UIButton with a custom image rather than a raw image view. A button gives you a normal target-action pathway while still letting you control the appearance.

swift
1import UIKit
2
3final class ItemsViewController: UITableViewController {
4    private let items = ["One", "Two", "Three"]
5
6    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
7        items.count
8    }
9
10    override func tableView(
11        _ tableView: UITableView,
12        cellForRowAt indexPath: IndexPath
13    ) -> UITableViewCell {
14        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")
15            ?? UITableViewCell(style: .default, reuseIdentifier: "Cell")
16
17        cell.textLabel?.text = items[indexPath.row]
18
19        let button = UIButton(type: .system)
20        button.setImage(UIImage(systemName: "info.circle"), for: .normal)
21        button.frame = CGRect(x: 0, y: 0, width: 28, height: 28)
22        button.addTarget(self, action: #selector(accessoryTapped(_:)), for: .touchUpInside)
23        cell.accessoryView = button
24
25        return cell
26    }
27
28    @objc private func accessoryTapped(_ sender: UIButton) {
29        let point = sender.convert(CGPoint.zero, to: tableView)
30        guard let indexPath = tableView.indexPathForRow(at: point) else { return }
31        print("Accessory tapped for row \\(indexPath.row)")
32    }
33}

This is the simplest robust pattern. The button owns the tap, and you resolve the tapped row by converting the button's position into the table view's coordinate space.

If You Want Delegate-Style Separation

Sometimes you want the cell to stay dumb and route the accessory action back to the controller in a cleaner way. A custom cell with a closure or protocol callback works well for that.

swift
1import UIKit
2
3final class ItemCell: UITableViewCell {
4    let accessoryButton = UIButton(type: .system)
5    var onAccessoryTap: (() -> Void)?
6
7    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
8        super.init(style: style, reuseIdentifier: reuseIdentifier)
9
10        accessoryButton.setImage(UIImage(systemName: "chevron.right.circle"), for: .normal)
11        accessoryButton.frame = CGRect(x: 0, y: 0, width: 28, height: 28)
12        accessoryButton.addTarget(self, action: #selector(handleAccessoryTap), for: .touchUpInside)
13        accessoryView = accessoryButton
14    }
15
16    required init?(coder: NSCoder) {
17        fatalError("init(coder:) has not been implemented")
18    }
19
20    @objc private func handleAccessoryTap() {
21        onAccessoryTap?()
22    }
23}

Then configure it in the table view controller:

swift
1override func tableView(
2    _ tableView: UITableView,
3    cellForRowAt indexPath: IndexPath
4) -> UITableViewCell {
5    let cell = tableView.dequeueReusableCell(withIdentifier: "ItemCell") as? ItemCell
6        ?? ItemCell(style: .default, reuseIdentifier: "ItemCell")
7
8    cell.textLabel?.text = items[indexPath.row]
9    cell.onAccessoryTap = { [weak self] in
10        guard let self else { return }
11        print("Accessory tapped for item: \\(self.items[indexPath.row])")
12    }
13
14    return cell
15}

This is easier to test and keeps the row-action logic readable.

Distinguish Row Selection from Accessory Taps

It is often useful to let a row tap do one thing and the accessory tap do something else. For example, tapping the row might open the item, while tapping the accessory could show details or a context menu.

You still use the normal delegate for row selection:

swift
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    print("Row selected: \\(items[indexPath.row])")
}

The accessory tap remains a separate control action. That separation is one of the main benefits of a custom accessory view.

Make the Control Feel Native

Custom accessory views are easy to implement badly. Keep a few details in mind:

  • use a UIButton or another UIControl, not a passive image view
  • give the control a tap target large enough for comfortable use
  • set an accessibility label so VoiceOver users know what the button does
  • avoid doing heavy work directly inside the tap handler

For example:

swift
button.accessibilityLabel = "Show item details"

That small addition matters if the icon alone does not explain the action.

Common Pitfalls

The most common mistake is expecting tableView(_:accessoryButtonTappedForRowWith:) to fire for a custom accessoryView. Another is using a UIImageView instead of a control, which leaves the icon visible but not properly interactive. Developers also sometimes rely on tag values for row lookup and forget that cell reuse can make that brittle if the tags are not updated carefully. A final issue is attaching the accessory action correctly but forgetting to preserve separate behavior between row selection and accessory taps, which makes the UI feel inconsistent.

Summary

  • A custom accessoryView does not automatically participate in the built-in accessory delegate callback.
  • Use a UIButton or another UIControl for a tappable custom image.
  • Resolve the row from the sender's position or route the event through a custom cell callback.
  • Keep row selection and accessory actions separate when they represent different intents.
  • Add accessibility metadata and a sensible touch target so the custom accessory behaves like a real iOS control.

Course illustration
Course illustration

All Rights Reserved.