Issue Detecting Button Cellforrowat

Issue Detecting Button cellForRowAt

The easiest and most efficient way in Swift is a callback closure.

  • Subclass UITableViewCell, the viewWithTag way to identify UI elements is outdated.
  • Set the class of the custom cell to the name of the subclass and set the identifier to ButtonCellIdentifier in Interface Builder.

  • Add a callback property.

  • Add an action and connect the button to the action.

    class ButtonCell: UITableViewCell {

    var callback : (() -> Void)?

    @IBAction func buttonPressed(_ sender : UIButton) {
    callback?()
    }
    }
  • In cellForRow assign the callback to the custom cell.

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "ButtonCellIdentifier", for: indexPath) as! ButtonCell
    cell.callback = {
    print("Button pressed", indexPath)
    }
    return cell
    }
  • When the button is pressed the callback is called. The index path is captured.

Edit

There is a caveat if cells can be added or removed. In this case pass the UITableViewCell instance as parameter and get the index path from there

class ButtonCell: UITableViewCell {

var callback : ((UITableViewCell) -> Void)?

@IBAction func buttonPressed(_ sender : UIButton) {
callback?(self)
}
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ButtonCellIdentifier", for: indexPath) as! ButtonCell
let item = dataSourceArray[indexPath.row]
// do something with item
cell.callback = { cell in
let actualIndexPath = tableView.indexPath(for: cell)!
print("Button pressed", actualIndexPath)
}
return cell
}

If even the section can change, well, then protocol/delegate may be more efficient.

Swift - How to detect an action button in UItableViewCell is pressed from ViewController?

You could go for a good old fashioned delegate pattern. This has the advantage of not coupling your view controller from your cell. Don't forget to make your delegate weak to avoid retain cycles.

The you can find the cell index path from the table view. (I'm assuming by cell number you mean index path)

protocol CellDelegate: class {
func didTap(_ cell: Cell)
}

class Cell: UITableViewCell {

weak var delegate: CellDelegate?
@IBAction func buttonPressed(_ sender: Any) {
delegate?.didTap(self)
}
}

class ViewController: UIViewController, CellDelegate {

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = ...
cell.delegate = self
return cell
}

func didTap(_ cell: Cell) {
let indexPath = self.tableView.indexPath(for: cell)
// do something with the index path
}
}

Detecting which UIButton was pressed in a UITableView

In Apple's Accessory sample the following method is used:

[button addTarget:self action:@selector(checkButtonTapped:) forControlEvents:UIControlEventTouchUpInside];

Then in touch handler touch coordinate retrieved and index path is calculated from that coordinate:

- (void)checkButtonTapped:(id)sender
{
CGPoint buttonPosition = [sender convertPoint:CGPointZero toView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:buttonPosition];
if (indexPath != nil)
{
...
}
}

Identify in which table view Cell button was pressed?

Use this line to get indexPath, Where you have to pass UIButton on target selector

func buttonTapped(_ sender:AnyObject) {
let buttonPosition:CGPoint = sender.convert(CGPointZero, to:self.tableView)
let indexPath = self.tableView.indexPathForRow(at: buttonPosition)
}

How to get the indexPath.row when I click a button on a table view cell?

A common, generalized solution for this type of problem is to connect the @IBAction of the button to a handler in the cell (not in the view controller), and then use a delegate-protocol pattern so the cell can tell the table when the button was tapped. The key is that when the cell does this, it will supply a reference to itself, which the view controller can then use to determine the appropriate indexPath (and thus the row).

For example:

  • Give your UITableViewCell subclass a protocol:

    protocol CustomCellDelegate: class {
    func cell(_ cell: CustomCell, didTap button: UIButton)
    }
  • Hook up the @IBAction to the cell (not the view controller) and have that call the delegate method:

    class CustomCell: UITableViewCell {
    weak var delegate: CustomCellDelegate?

    @IBOutlet weak var customLabel: UILabel!

    func configure(text: String, delegate: CustomCellDelegate) {
    customLabel.text = text
    self.delegate = delegate
    }

    @IBAction func didTapButton(_ button: UIButton) {
    delegate?.cell(self, didTap: button)
    }
    }
  • Obviously, when the cell is created, call the configure method, passing, amongst other things, a reference to itself as the delegate:

    extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { ... }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
    let text = ...
    cell.configure(text: text, delegate: self)
    return cell
    }
    }
  • Finally, have the delegate method call indexPath(for:) to determine the index path for the cell in question:

    extension ViewController: CustomCellDelegate {
    func cell(_ cell: CustomCell, didTap button: UIButton) {
    guard let indexPath = tableView.indexPath(for: cell) else { return }

    // use `indexPath.row` here
    }
    }

The other approach is to use closures, but again using the same general pattern of hooking the button @IBAction to the cell, but have it call a closure instead of the delegate method:

  • Define custom cell with closure that will be called when the button is tapped:

    class CustomCell: UITableViewCell {
    typealias ButtonHandler = (CustomCell) -> Void

    var buttonHandler: ButtonHandler?

    @IBOutlet weak var customLabel: UILabel!

    func configure(text: String, buttonHandler: @escaping ButtonHandler) {
    customLabel.text = text
    self.buttonHandler = buttonHandler
    }

    @IBAction func didTapButton(_ button: UIButton) {
    buttonHandler?(self)
    }
    }
  • When the table view data source creates the cell, supply a handler closure:

    extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { ... }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
    let text = ...
    cell.configure(text: text, buttonHandler: { [weak self] cell in // the `[weak self]` is only needed if this closure references `self` somewhere
    guard let indexPath = tableView.indexPath(for: cell) else { return }

    // use `indexPath` here
    })
    return cell
    }
    }

I personally prefer the delegate-protocol pattern, as it tends to scale more nicely, but both approaches work.


Note, in both examples, I studiously avoided saving the indexPath in the cell, itself (or worse, “tag” values). By doing this, it protects you from getting misaligned if rows are later inserted and deleted from the table.


By the way, I used fairly generic method/closure names. In a real app, you might give them more meaningful names, e.g., didTapInfoButton, didTapSaveButton, etc.) that clarifies the functional intent.



Related Topics



Leave a reply



Submit