22

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

jack87
  • 465
  • 2
  • 5
  • 13
  • 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. – alexburtnik Oct 09 '16 at 18:38

9 Answers9

86

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.

Community
  • 1
  • 1
pedrouan
  • 12,762
  • 3
  • 58
  • 74
  • 1
    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 – jack87 Oct 09 '16 at 19:23
  • You may only need to add a connection between the button from the cell and the IBAction buttonPressed() method typed in the step 2) – pedrouan Oct 09 '16 at 19:25
  • 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 – jack87 Oct 09 '16 at 19:27
  • 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 – pedrouan Oct 09 '16 at 19:30
  • 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 – jack87 Oct 09 '16 at 19:35
  • You're welcome! I've already added the line, for completness, as it could confuse. Thanks. – pedrouan Oct 09 '16 at 19:38
  • 5
    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. – MSurrow Dec 06 '16 at 12:28
  • 3
    How do you resolve the section part of cell's index path in case the tableview implements also sections? – valeCocoa Jul 31 '17 at 11:34
  • Using the tag property is huge clutch, always try to avoid .tag – hashier Sep 14 '17 at 16:17
  • 1
    To handle multiple section give your cell an indexPath var and set that instead of using tag. – aaronium112 Dec 06 '17 at 14:44
  • 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. – user3344977 Jun 12 '19 at 17:56
  • 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){ } } – Lax Apr 30 '20 at 21:27
26

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 
  }
}
RimKtari
  • 261
  • 3
  • 7
19

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
    }
}
Frederico
  • 203
  • 3
  • 8
15

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.

Abhishek Mitra
  • 3,335
  • 4
  • 25
  • 46
11

@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
        }
    }
}
Changnam Hong
  • 1,669
  • 18
  • 29
  • 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 Jun 12 '19 at 17:54
  • This is rly good! Nice :)) – tBug Oct 07 '21 at 14:31
3

You can also get selected button index using tableViewCell view's hierarchy.

Using following steps :

  1. add selector to the cellForRowAtIndexpath of tableview :

    btn?.addTarget(self, action:#selector(buttonPressed(_:)), for:.touchUpInside)
    

2 . get indexPath using following method :

func buttonPressed(_ sender: AnyObject) {
        let button = sender as? UIButton
        let cell = button?.superview?.superview as? UITableViewCell
        let indexPath = tblview.indexPath(for: cell!)
        print(indexPath?.row)
    }
KKRocks
  • 8,222
  • 1
  • 18
  • 84
  • 1
    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. – valeCocoa Jul 31 '17 at 11:37
1

UIButton in Tableview cell. Programically create action method in swift 4.2

cell.btnlike.addTarget(self, action: #selector(buttonbtnlikePressed(_:event:)), for: .touchUpInside)


@objc func buttonbtnlikePressed(_ sender: Any, event: Any) {
        let point : CGPoint = (sender as AnyObject).convert(CGPoint.zero, to:tblPost)
        var indexPath =  self.tblPost!.indexPathForRow(at: point)
        if let btnlike = sender as? UIButton{
            if btnlike.isSelected{
                btnlike.isSelected = false
            }else{
                btnlike.isSelected = true
            }
        }
    }
Himanshu Moradiya
  • 4,769
  • 4
  • 25
  • 49
0

if you are creating table view cell programmatically you need to add add subview to contentView not cell itself

Akhadjon
  • 1
  • 1
0

Swift 5

First thing create the protocol(delegate).

protocol CustomDelegate: AnyObject{
  
  func didSelectedTheButton(_ index: Int)
  
}

Create the UITableViewCell and initialize the protocol as instance. And add button and button action in your TableViewCell. Use the protocol (delegate) in the button Action. Please find the below codes for reference

class TableViewCell: UITableViewCell {

  lazy var button: UIButton! = {

    let button = UIButton()
    button.translatesAutoresizingMaskIntoConstraints = false
    button.backgroundColor = .darkGray
    button.setTitleColor(.white, for: .normal)
    button.addTarget(self, action: #selector(didTaped(_:)), for: .touchUpInside)
    button.tag = 1
    button.setTitle("Tap", for: .normal)
    return button

  }()
  
  weak var delegate: CustomDelegate?

  override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)
    
    self.contentView.backgroundColor =  .clear
    self.backgroundColor =  .clear
    self.contentView.addSubview(button)

    self.button.heightAnchor.constraint(equalToConstant: 60).isActive = true
    self.button.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width * 0.66).isActive = true
    self.button.centerXAnchor.constraint(equalTo: self.contentView.centerXAnchor).isActive = true
    self.button.centerYAnchor.constraint(equalTo: self.contentView.centerYAnchor).isActive = true
    
  }
  
  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
  
  @objc func didTaped(_ sender: UIButton){

    //Call the protocol function for receive the action in ViewController
    delegate?.didSelectedTheButton(sender.tag)
    
  }

}

Next create the UIViewController and integrate the UITableView in your ViewController. In function tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell assign your viewcontrollor to delegate which initialise in UITableViewCell

class SecondViewController: UIViewController {

  lazy var tableView: UITableView! = {
      let tableView = UITableView(frame: .zero, style: .grouped)
      tableView.translatesAutoresizingMaskIntoConstraints = false
      tableView.register(TableViewCell.self, forCellReuseIdentifier: String(describing: TableViewCell.self))
      tableView.backgroundColor   =   .white
      tableView.separatorColor    =   .clear
      tableView.delegate          =   self
      tableView.dataSource        =   self
      return tableView
  }()

    override func viewDidLoad() {
        super.viewDidLoad()
      
      self.view.backgroundColor = .white
      
      //
      self.view.addSubview(self.tableView)
      
      NSLayoutConstraint.activate([
      
        self.tableView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
        self.tableView.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor),
        self.tableView.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor),
        self.tableView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor),

      ])

    }
    
}

extension SecondViewController: UITableViewDelegate, UITableViewDataSource{
  
  func numberOfSections(in tableView: UITableView) -> Int {
    return 1
  }
  
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return 10
  }
  
  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    
    let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TableViewCell.self), for: indexPath) as! TableViewCell
    cell.button.tag = indexPath.row

    //ViewController Assign for protocol delegate
    cell.delegate = self

    cell.button.setTitle("Button \(indexPath.row)", for: .normal)
    return cell
    
  }
  
  func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    return 100
  }
  
}

extension SecondViewController: CustomDelegate{

  //Received the button action for cell
     /**
        index - contains row value for button selected
     */
  func didSelectedTheButton(_ index: Int) {

     // Enter your code here what did you want when the tableViewCell button action
     print(index)
    
  }
  
}