UITableViewCell Buttons with action

50,257

Solution 1

I was resolving this using a cell delegate method within UITableViewCell's subclass.

Quick overview:

1) Create a protocol

protocol YourCellDelegate : class {
    func didPressButton(_ tag: Int)
}

2) Subclass your UITableViewCell (if you haven't done so):

class YourCell : UITableViewCell
{
     var cellDelegate: YourCellDelegate?   
      @IBOutlet weak var btn: UIButton!
    // connect the button from your cell with this method
    @IBAction func buttonPressed(_ sender: UIButton) {
        cellDelegate?.didPressButton(sender.tag)
    }         
    ...
}

3) Let your view controller conform to YourCellDelegate protocol that was implemented above.

class YourViewController: ..., YourCellDelegate {  ... }

4) Set a delegate, after the cell has been defined (for reusing).

let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! YourCell
cell.cellDelegate = self
cell.btn.tag = indexPath.row

5) In the same controller (where is your implemented UITableView delegate/datasource), put a method from YourCellDelegate protocol.

func didPressButton(_ tag: Int) {
     print("I have pressed a button with a tag: \(tag)")
}

Now, your solution is not tag / number dependent. You can add as many buttons as you want, so you are ready to get response via delegate regardless how many buttons you want to install.

This protocol-delegate solution is preferred in iOS logic and it can be used for other elements in table cell, like UISwitch, UIStepper, and so on.

Solution 2

swift 4.2

You can also use closures instead of delegates

1) In your UITableViewCell :

 class ExampleCell: UITableViewCell {
    //create your closure here  
         var buttonPressed : (() -> ()) = {}

        @IBAction func buttonAction(_ sender: UIButton) {
    //Call your closure here 
            buttonPressed()
        }
    }

2) In your ViewController

class ViewController:  UIViewController,  UITableViewDataSource, UITableViewDelegate {
 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
 let cell = tableView.dequeueReusableCell(withIdentifier: "ExampleCell", for: indexPath) as! ExampleCell
   cell.buttonPressed = {
          //Code
           }
return cell 
  }
}

Solution 3

I came across the same problem after making the IBOutlets private as has been broadly suggested by the community.

Here is my solution:

< In your cell class >

protocol YourCellDelegate: class {
    func didTapButton(_ sender: UIButton)
}

class YourCell: UITableViewCell {

    weak var delegate: YourCellDelegate?

    @IBAction func buttonTapped(_ sender: UIButton) {
        delegate?.didTapButton(sender)
    }
}

< In your ViewController >

class ViewController: UIViewController, YourCellDelegate {

    func didTapButton(_ sender: UIButton) {
        if let indexPath = getCurrentCellIndexPath(sender) {
            item = items[indexPath.row]
        }
    }

    func getCurrentCellIndexPath(_ sender: UIButton) -> IndexPath? {
        let buttonPosition = sender.convert(CGPoint.zero, to: tableView)
        if let indexPath: IndexPath = tableView.indexPathForRow(at: buttonPosition) {
            return indexPath
        }
        return nil
    }
}

Solution 4

SWIFT 4.*

It can be done like following way too, Not required much coding and delegation, Simple and easy.

Put following code in cellForItemAt for UICollectionView or in cellForRowAt for UITableView

cell.btn.tag = indexPath.row
cell.btn.addTarget(self, action: #selector(buttonSelected), for: .touchUpInside)

And your Method will be

@objc func buttonSelected(sender: UIButton){
    print(sender.tag)
}

Thats all.

Solution 5

@pedrouan is great, except using button's tag option. In many cases, when you set button on tableViewCell, those buttons will modify tableView dataSource.(e.g. InsertRow, DeleteRow).

But the tag of the button is not updated even if a new cell is inserted or deleted. Therefore, it is better to pass the cell itself as a parameter rather than passing the button's tag to the parameter.

Here is my example to achieve this.

Your ExampleCell

protocol ExampleCellDelegate: class {
    func didTapButton(cell: ExampleCell)
}

class ExampleCell: UITableViewCell {

    weak var cellDelegate: ExampleCellDelegate?

    @IBAction func btnTapped(_ sender: UIButton) {
        cellDelegate?.didTapButton(cell: self)
    }
}

Your ViewController

class ViewController: ExampleCellDelegate {
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        if let cell = tableView.dequeueReusableCell(withIdentifier: "ExampleCell", for: indexPath) as? ExampleCell {

            cell.cellDelegate = self
            return cell
        }
        return UITableViewCell()
    }

    func didTapButton(cell: ExampleCell) {
        if let indexPath = tableView.indexPath(for: cell) {
            // do Something
        }
    }
}
Share:
50,257

Related videos on Youtube

jack87
Author by

jack87

Updated on December 27, 2021

Comments

  • jack87
    jack87 over 2 years

    Hi I have a custom UITableViewCell with three buttons to handle a shopping cart function, Plus,Minus and Delete button and I need to know which cell has been touched.

    I've already tried to use the "tag solution" but it isn't working due to the lifecycle of the cells.

    Can anyone please help me find a solution?

    Thanks in advance

    • alexburtnik
      alexburtnik over 7 years
      Please provide some code of how exactly you're using tags. If you set them in cellForRowAtIndexPath every time a cell is reused , there should not be any problems with lifecycle.
  • jack87
    jack87 over 7 years
    This solution seems to be amazing, but I still need to know which cell I've pressed, and when using the tag argument the problem is still the same
  • pedrouan
    pedrouan over 7 years
    You may only need to add a connection between the button from the cell and the IBAction buttonPressed() method typed in the step 2)
  • jack87
    jack87 over 7 years
    This solution seems to be amazing, but I still need to know which cell I've pressed, and when using the tag argument the problem is still the same
  • pedrouan
    pedrouan over 7 years
    And when you try to set 'cell.tag = indexPath.row' inside cellForRow method, after .delegate row within the step 4)? If you have set cell prototype specific under each button, it can be a bit different
  • jack87
    jack87 over 7 years
    My bad, I made a typo and I've actually deleted the "cell.tag = indexPath.row", now your solution is perfectly working. Thanks a lot man
  • pedrouan
    pedrouan over 7 years
    You're welcome! I've already added the line, for completness, as it could confuse. Thanks.
  • MSurrow
    MSurrow over 7 years
    The cell.tag = indexPath.row just gives me 0 for all rows. Shouldn't the sender.tag in step 2 be self.tag, since it is the cell's tag you set to indexPath.row in step 4. Using self.tag instead of sender.tag in step 2 works as expected for me.
  • valeCocoa
    valeCocoa almost 7 years
    Your solution should be the one accepted. It also works without using a delegation pattern for the UITableViewCell subclass when configuring the cell's button to be tied to an action of a view controller.
  • valeCocoa
    valeCocoa almost 7 years
    How do you resolve the section part of cell's index path in case the tableview implements also sections?
  • valeCocoa
    valeCocoa almost 7 years
    This is a good solution but strongly thighed to how the view is designed. In case the button lies in another subview (as for example a stack view), you'd might get with a long list of superview calls to detect the cell where the button lies in.
  • hashier
    hashier over 6 years
    So much nicer to not use .tag property and so much cleaner!
  • hashier
    hashier over 6 years
    Using the tag property is huge clutch, always try to avoid .tag
  • hashier
    hashier over 6 years
    Another way instead of convert points is to use superview to get the cell and then call indexPath(for: UITableViewCell)
  • DeyaEldeen
    DeyaEldeen over 6 years
    you forgot cell.delegate = self in cellforrowatindexpath.
  • Aaronium112
    Aaronium112 over 6 years
    To handle multiple section give your cell an indexPath var and set that instead of using tag.
  • Anthony Saltarelli
    Anthony Saltarelli over 6 years
    Thank you @DeyaEldeen - that is the only other thing needed.
  • user3344977
    user3344977 almost 5 years
    This is the only correct answer that includes best practices, which is funny because it has zero upvotes. Just look at examples like UITableViewDelegate for guidance. You should be passing the cell itself in the delegate method. Then, in your controller for example, you can fetch the index path for the cell, which can then be used to access the correct data in your data source via indexPath.row. Relying on a primitive like tag makes no sense and is bound to break.
  • user3344977
    user3344977 almost 5 years
    This solution is close, but falls short. You should not be using or passing tag. You should be passing the cell itself, as well as a piece of relevant data if you wish. Just look at UITableViewDelegate for guidance. The answer you're looking for is already there. Relying on a primitive like tag is not a good practice and is bound to break and cause problems.
  • Basant
    Basant about 4 years
    thanks @RimK its working fine +1 up for this solution
  • Lax
    Lax about 4 years
    Instead of tag, I send the sender (button) and calculate the index path as below.Now you can have he section and row of cell. --> func didPressButton(_ sender: UIButton) { let buttonPosition = sender.convert(CGPoint.zero, to: tableView) if let index = tableView.indexPathForRow(at: buttonPosition){ } }
  • tBug
    tBug over 2 years
    This is rly good! Nice :))