146

I have a view controller with a table view and a separate nib for the table cell template. The cell template has some buttons. I want to access the button click along with the index of the cell clicked inside the view controller where I have defined the Table view.

So I have ViewController.h and ViewController.m where I have the UITableView and TableTemplate.h, TableTemplate.m and TableTemplate.xib where I have the nib defined. I want the button click event with cell index in ViewController.m.

Any help on how can I do that?

Tamás Sengel
  • 55,884
  • 29
  • 169
  • 223
ankit_rck
  • 1,796
  • 2
  • 14
  • 24
  • Another approach posted here: https://stackoverflow.com/a/68512342/3276518 involving subclassing the button and adding a weak pointer to the parent cell. – bauerMusic Jul 24 '21 at 17:38

18 Answers18

264

1) In your cellForRowAtIndexPath: method, assign button tag as index:

cell.yourbutton.tag = indexPath.row;

2) Add target and action for your button as below:

[cell.yourbutton addTarget:self action:@selector(yourButtonClicked:) forControlEvents:UIControlEventTouchUpInside];

3) Code actions based on index as below in ViewControler:

-(void)yourButtonClicked:(UIButton*)sender
{
     if (sender.tag == 0) 
     {
         // Your code here
     }
}

Updates for multiple Section:

You can check this link to detect button click in table view for multiple row and section.

Community
  • 1
  • 1
Mani
  • 17,549
  • 13
  • 79
  • 100
  • 1
    This can also be done through Interface Builder (IB) in step two. Just make sure your buttons tag is set. You really don't want to mix up your action calling. Either do it through IB or do it explicitly in your code. – Sententia Apr 29 '14 at 07:12
  • @Mani It doesn't break MVC - the action is in the TableView not the Cell. – davecom May 31 '14 at 04:08
  • @davecom If you set button target as cell(via IB), How it will be trigger from tableView? Or Is their any way to connect button target to tableview which is placed in cell's xib? – Mani May 31 '14 at 04:17
  • @Mani Add a IBAction to the tableview and connect to it from the button in the cell. – davecom May 31 '14 at 06:41
  • I think, this is worst case. Connecting from cell's xib to tableview which is lie on other class. – Mani May 31 '14 at 06:58
  • Nope, pretty standard pattern actually. – davecom Jun 02 '14 at 05:32
  • Hi I have three button and 3 label (associated with each button) in each table view cell. Now I have to change text color of perticular label( associated with button) on button click how can I please suggest me – Gajendra Rawat Aug 04 '14 at 06:55
  • Take a look into this link about custom table view cell http://www.appcoda.com/customize-table-view-cells-for-uitableview/. And now you could create custom table view cell which have three label with associative button that could place on label as invisible with custom button type. Write IBAction method for each button in table view cell and wired up with corresponding button which lie on label. I think, it will make sense and easiest way to do this. – Mani Aug 04 '14 at 07:03
  • Is this approach correct? At least it seems incorrect to assign a tag value starting from zero. – user2159978 Nov 24 '14 at 07:33
  • @user2159978 In what case, it seems to be incorrect? could you please explain? – Mani Nov 24 '14 at 10:32
  • does the system uses tags by itself to enumerate controls? Or even if no then what is the default tag value? Maybe zero which is used in this example? – user2159978 Nov 24 '14 at 10:37
  • @user2159978 did you see this line `cell.yourbutton.tag = indexPath.row;`. You've to initialize tag before use it. Otherwise it leads to unexpected result. – Mani Nov 24 '14 at 10:55
  • the question was about does iOS system use tags which you haven't set by itself? and may these values correlate with tag = 0 you set in your own code? – user2159978 Nov 24 '14 at 11:23
  • 27
    This solution runs into problems when you start inserting and deleting rows. The tag is not updated when rows are shifted. Instead of keeping a reference to the row. It may be better to keep a reference to a unique object id. – Vincent Cheong Jun 12 '15 at 12:24
  • check this question dude http://stackoverflow.com/questions/31649220/detect-button-click-in-table-view-ios-xcode-for-multiple-row-and-section – Nischal Hada Jul 27 '15 at 09:20
  • @nischalhada This trick shouldn't work for your question (Multiple sections). You should go with another approach. – Mani Jul 27 '15 at 10:01
  • 1
    Anytime you find yourself assigning values to the tag attributes of views, you have a very bad code smell that can bite you later on. Look for better ways to achieve your goal, not the first S.O. post you find. – TigerCoding Dec 29 '15 at 19:44
  • @TigerCoding I agree with the point "Look for better ways to achieve" But It's not too bad code, if so, S.O user won't give votes like this and also that cann't bit you later on. If you doing wrongly, its bite you right now. I know, This code won't help you for section wise datasource. See older comments. – Mani Dec 30 '15 at 07:02
152

Delegates are the way to go.

As seen with other answers using views might get outdated. Who knows tomorrow there might be another wrapper and may need to use cell superview]superview]superview]superview]. And if you use tags you would end up with n number of if else conditions to identify the cell. To avoid all of that set up delegates. (By doing so you will be creating a re usable cell class. You can use the same cell class as a base class and all you have to do is implement the delegate methods.)

First we need a interface (protocol) which will be used by cell to communicate(delegate) button clicks. (You can create a separate .h file for protocol and include in both table view controller and custom cell classes OR just add it in custom cell class which will anyway get included in table view controller)

@protocol CellDelegate <NSObject>
- (void)didClickOnCellAtIndex:(NSInteger)cellIndex withData:(id)data;
@end

Include this protocol in custom cell and table view controller. And make sure table view controller confirms to this protocol.

In custom cell create two properties :

@property (weak, nonatomic) id<CellDelegate>delegate;
@property (assign, nonatomic) NSInteger cellIndex;

In UIButton IBAction delegate click : (Same can be done for any action in custom cell class which needs to be delegated back to view controller)

- (IBAction)buttonClicked:(UIButton *)sender {
    if (self.delegate && [self.delegate respondsToSelector:@selector(didClickOnCellAtIndex:withData:)]) {
        [self.delegate didClickOnCellAtIndex:_cellIndex withData:@"any other cell data/property"];
    }
}

In table view controller cellForRowAtIndexPath after dequeing the cell, set the above properties.

cell.delegate = self;
cell.cellIndex = indexPath.row; // Set indexpath if its a grouped table.

And implement the delegate in table view controller:

- (void)didClickOnCellAtIndex:(NSInteger)cellIndex withData:(id)data
{
    // Do additional actions as required.
    NSLog(@"Cell at Index: %d clicked.\n Data received : %@", cellIndex, data);
}

This would be the ideal approach to get custom cell button actions in table view controller.

GoodSp33d
  • 6,252
  • 4
  • 35
  • 67
  • 3
    Why have you made the delegate a strong property of the cell? This will give you a retain cycle, unless you know the controller only weakly holds the cell. – JulianSymes Jul 17 '14 at 11:17
  • what about the _cellIndex beign updated after the cell is deleted? – skornos Mar 07 '15 at 09:17
  • 2
    I heard from one of my friend saying using delegate on each cell causes memory consumption, so use tags. Is this true? – Bista Jul 10 '15 at 04:38
  • 2
    check this question dude http://stackoverflow.com/questions/31649220/detect-button-click-in-table-view-ios-xcode-for-multiple-row-and-section – Nischal Hada Jul 27 '15 at 09:21
  • @the_UB There can't be much between setting a tag and storing a single reference. Possibly a tag would take up more memory. – Ian Warburton Nov 30 '15 at 15:41
  • Clean solution @GoodSp33d , however your `UITableViewCell` shouldn't be returning any `(id)data` in it's protocol, since this would violate the MVC principle, where the View is also handling the Model. Not to mention that cells are meant to be reusable, and thus should be as decoupled as possible from their parent views. Once the cell returns it's `cellIndex`, the UIViewController conforming to it's protocol (and acting as the `UITableView`'s parent), should handle fetching the proper data for the cell, from the `UITableView`'s datasource. – Priest Dec 20 '16 at 05:22
  • Storing the index path isn't necessary, because you can just call tableView.indexPath(for: cell) – Learn OpenGL ES Jun 05 '17 at 23:44
  • This should be the top answer. Delegation is far cleaner. – Charlie S Dec 10 '19 at 12:52
71

Instead of playing with tags, I took different approach. Made delegate for my subclass of UITableViewCell(OptionButtonsCell) and added an indexPath var. From my button in storyboard I connected @IBAction to the OptionButtonsCell and there I send delegate method with the right indexPath to anyone interested. In cell for index path I set current indexPath and it works :)

Let the code speak for itself:

Swift 3 Xcode 8

OptionButtonsTableViewCell.swift

import UIKit
protocol OptionButtonsDelegate{
    func closeFriendsTapped(at index:IndexPath)
}
class OptionButtonsTableViewCell: UITableViewCell {
    var delegate:OptionButtonsDelegate!
    @IBOutlet weak var closeFriendsBtn: UIButton!
    var indexPath:IndexPath!
    @IBAction func closeFriendsAction(_ sender: UIButton) {
        self.delegate?.closeFriendsTapped(at: indexPath)
    }
}

MyTableViewController.swift

class MyTableViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, OptionButtonsDelegate {...

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "optionCell") as! OptionButtonsTableViewCell
    cell.delegate = self
    cell.indexPath = indexPath
    return cell   
}

func closeFriendsTapped(at index: IndexPath) {
     print("button tapped at index:\(index)")
}
Maciej Chrzastek
  • 1,380
  • 12
  • 18
  • can you help me, i am getting error at this line: `class MyTableViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, OptionButtonsDelegate` //error: Redundant conformance of 'MyTableViewController' to protocol 'UITableViewDataSource' – Ulug'bek Jan 05 '17 at 06:10
  • looks like you are trying to conform to UITableViewDataSource multiple times. Perhaps you have got an extension where you already conform to data source?, can't help more without code – Maciej Chrzastek Jan 05 '17 at 11:29
  • 1
    and how to pass data to perform segue and go to another view controller ? – Milad Faridnia Mar 07 '17 at 12:45
  • 3
    Best and cleanest solution! – appsunited Sep 07 '18 at 17:26
  • Why is this not the preferred solution? It should be. Thanks for a clean/safe solution with well documented code and explanation. –  Feb 16 '21 at 02:10
31

This should help :-

UITableViewCell* cell = (UITableViewCell*)[sender superview];
NSIndexPath* indexPath = [myTableView indexPathForCell:cell];

Here sender is the UIButton instance that is sending the event. myTableView is the UITableView instance you're dealing with.

Just get the cell reference right and all the work is done.

You may need to remove the buttons from cell's contentView & add them directly to UITableViewCell instance as it's subview.

Or

You can formulate a tag naming scheme for different UIButtons in cell.contentView. Using this tag, later you can know the row & section information as needed.

Tarun
  • 356
  • 2
  • 6
23

Following code might Help you.

I have taken UITableView with custom prototype cell class named UITableViewCell inside UIViewController.

So i have ViewController.h, ViewController.m and TableViewCell.h,TableViewCell.m

Here is the code for that:

ViewController.h

@interface ViewController : UIViewController<UITableViewDataSource,UITableViewDelegate>

@property (strong, nonatomic) IBOutlet UITableView *tblView;

@end

ViewController.m

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    return (YourNumberOfRows);
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{

    static NSString *cellIdentifier = @"cell";

    __weak TableViewCell *cell = (TableViewCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];

    if (indexPath.row==0) {
        [cell setDidTapButtonBlock:^(id sender)
         {
             // Your code here

         }];
    }    
    return cell;
}

Custom cell class :

TableViewCell.h

@interface TableViewCell : UITableViewCell

@property (copy, nonatomic) void (^didTapButtonBlock)(id sender);

@property (strong, nonatomic) IBOutlet UILabel *lblTitle;
@property (strong, nonatomic) IBOutlet UIButton *btnAction;

- (void)setDidTapButtonBlock:(void (^)(id sender))didTapButtonBlock;

@end

and

UITableViewCell.m

@implementation TableViewCell

- (void)awakeFromNib {
    // Initialization code
    [self.btnAction addTarget:self action:@selector(didTapButton:) forControlEvents:UIControlEventTouchUpInside];

}

- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
    [super setSelected:selected animated:animated];

    // Configure the view for the selected state
}
- (void)didTapButton:(id)sender {
    if (self.didTapButtonBlock)
    {
        self.didTapButtonBlock(sender);
    }
}

Note: Here I have taken all UIControls using Storyboard.

Hope that can help you...!!!

Piyush
  • 1,534
  • 12
  • 32
16

Use Swift closures :

class TheCell: UITableViewCell {

    var tapCallback: (() -> Void)?

    @IBAction func didTap(_ sender: Any) {
        tapCallback?()
    }
}

extension TheController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: TheCell.identifier, for: indexPath) as! TheCell {
            cell.tapCallback = {
                //do stuff
            }
            return cell
    }
}
valexa
  • 4,462
  • 32
  • 48
15

The reason i like below technique because it also help me to identify the section of table.

Add Button in cell cellForRowAtIndexPath:

 UIButton *selectTaskBtn = [UIButton buttonWithType:UIButtonTypeCustom];
        [selectTaskBtn setFrame:CGRectMake(15, 5, 30, 30.0)];
        [selectTaskBtn setTag:indexPath.section]; //Not required but may find useful if you need only section or row (indexpath.row) as suggested by MR.Tarun 
    [selectTaskBtn addTarget:self action:@selector(addTask:)   forControlEvents:UIControlEventTouchDown];
[cell addsubview: selectTaskBtn];

Event addTask:

-(void)addTask:(UIButton*)btn
{
    CGPoint buttonPosition = [btn convertPoint:CGPointZero toView:self.tableView];
    NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:buttonPosition];
    if (indexPath != nil)
    {
     int currentIndex = indexPath.row;
     int tableSection = indexPath.section;
    }
}

Hopes this help.

Yogesh Lolusare
  • 2,162
  • 1
  • 24
  • 35
  • check this question dude http://stackoverflow.com/questions/31649220/detect-button-click-in-table-view-ios-xcode-for-multiple-row-and-section – Nischal Hada Jul 27 '15 at 09:21
7

Tarun's code doesnt work on iOS7, since the UITableViewCell structure changed and now he would get "UITableViewCellScrollView" instead.

This post Getting UITableViewCell with superview in iOS 7 has a good solution creating a loop to find the correct parent view, regardless of any future changes in the structure. It boils down to creating a loop:

    UIView *superView = [sender superview];
    UIView *foundSuperView = nil;

    while (nil != superView && nil == foundSuperView) {
        if ([superView isKindOfClass:[UITableViewCell class]]) {
            foundSuperView = superView;
        } else {
            superView = superView.superview;
        }
    }

The link has code for a more reusable solution, but this should work.

Community
  • 1
  • 1
Stenio Ferreira
  • 357
  • 2
  • 10
7

Its Work For me.

 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
     UIButton *Btn_Play = (UIButton *)[cell viewWithTag:101];
     [Btn_Play addTarget:self action:@selector(ButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
}
-(void)ButtonClicked:(UIButton*)sender {
     CGPoint buttonPosition = [sender convertPoint:CGPointZero toView:self.Tbl_Name];
     NSIndexPath *indexPath = [self.Tbl_Name indexPathForRowAtPoint:buttonPosition];
}
6

Swift 2.2

You need to add target for that button.

myButton.addTarget(self, action: #selector(ClassName.FunctionName(_:), forControlEvents: .TouchUpInside)

FunctionName: connected // for example

And of course you need to set tag of that button since you are using it.

myButton.tag = indexPath.row

You can achieve this by subclassing UITableViewCell. Use it in interface builder, drop a button on that cell, connect it via outlet and there you go.

To get the tag in the connected function:

func connected(sender: UIButton) {
    let buttonTag = sender.tag
    // Do any additional setup
}
Community
  • 1
  • 1
Himanshu padia
  • 7,428
  • 1
  • 47
  • 45
6

Swift 3 with a Closure

A nice solution is using a closure in a custom UITableViewCell to callback to the viewController for an action.

In cell:

final class YourCustomCell: UITableViewCell {

    var callbackClosure: (() -> Void)?

    // Configure the cell here
    func configure(object: Object, callbackClosure: (() -> Void)?) {
       self.callbackClosure = callbackClosure
    }


// MARK: - IBAction
extension YourCustomCell {
    @IBAction fileprivate func actionPressed(_ sender: Any) {
        guard let closure = callbackClosure else { return }
        closure()
    }
}

In View Controller: Tableview Delegate

extension YourViewController: UITableViewDelegate {

    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        guard let cell: YourCustomCell = cell as? YourCustomCell else { return }
        cell.configure(object: object, callbackClosure: { [weak self] in
            self?.buttonAction()
        })
     }
 }

fileprivate extension YourViewController {

    func buttonAction() {
        // do your actions here 
    }
}
Sevy11
  • 160
  • 4
  • 9
5

I find it simplest to subclass the button inside your cell (Swift 3):

class MyCellInfoButton: UIButton {
    var indexPath: IndexPath?
}

In your cell class:

class MyCell: UICollectionViewCell {
    @IBOutlet weak var infoButton: MyCellInfoButton!
   ...
}

In the table view's or collection view's data source, when dequeueing the cell, give the button its index path:

cell.infoButton.indexPath = indexPath

So you can just put these code into your table view controller:

@IBAction func handleTapOnCellInfoButton(_ sender: MyCellInfoButton) {
        print(sender.indexPath!) // Do whatever you want with the index path!
}

And don't forget to set the button's class in your Interface Builder and link it to the handleTapOnCellInfoButton function!


edited:

Using dependency injection. To set up calling a closure:

class MyCell: UICollectionViewCell {
    var someFunction: (() -> Void)?
    ...
    @IBAction func didTapInfoButton() {
        someFunction?()
    }
}

and inject the closure in the willDisplay method of the collection view's delegate:

 func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    (cell as? MyCell)?.someFunction = {
        print(indexPath) // Do something with the indexPath.
    }
}
yesleon
  • 928
  • 11
  • 14
2

If you want to pass parameter value from cell to UIViewController using closure then

//Your Cell Class
class TheCell: UITableViewCell {

    var callBackBlockWithParam: ((String) -> ()) = {_ in }

//Your Action on button
    @IBAction func didTap(_ sender: Any) {
        callBackBlockWithParam("Your Required Parameter like you can send button as sender or anything just change parameter type. Here I am passing string")
    }
}

//Your Controller
extension TheController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: TheCell.identifier, for: indexPath) as! TheCell {
            cell.callBackBlockWithParam = { (passedParamter) in 

             //you will get string value from cell class
                print(passedParamter)     
      }
            return cell
    }
}
prachit
  • 365
  • 3
  • 10
1
// Add action in cell for row at index path -tableView

cell.buttonName.addTarget(self, action: #selector(ViewController.btnAction(_:)), for: .touchUpInside)

// Button Action

  @objc func btnAction(_ sender: AnyObject) {



        var position: CGPoint = sender.convert(.zero, to: self.tableView)


        let indexPath = self.tableView.indexPathForRow(at: position)
        let cell: UITableViewCell = tableView.cellForRow(at: indexPath!)! as
        UITableViewCell




}
Hitesh Chauhan
  • 1,520
  • 15
  • 16
1

for swift 4:

inside the cellForItemAt ,
   
cell.chekbx.addTarget(self, action: #selector(methodname), for: .touchUpInside)

then outside of cellForItemAt
@objc func methodname()
{
//your function code
}
Radhe Yadav
  • 112
  • 11
0

@Mani answer is good, however tags of views inside cell's contentView often are used for other purposes. You can use cell's tag instead (or cell's contentView tag):

1) In your cellForRowAtIndexPath: method, assign cell's tag as index:

cell.tag = indexPath.row; // or cell.contentView.tag...

2) Add target and action for your button as below:

[cell.yourbutton addTarget:self action:@selector(yourButtonClicked:) forControlEvents:UIControlEventTouchUpInside];

3) Create method that returns row of the sender (thanks @Stenio Ferreira):

- (NSInteger)rowOfSender:(id)sender
{
    UIView *superView = sender.superview;
    while (superView) {
        if ([superView isKindOfClass:[UITableViewCell class]])
            break;
        else
            superView = superView.superview;
    }

    return superView.tag;
}

4) Code actions based on index:

-(void)yourButtonClicked:(UIButton*)sender
{
     NSInteger index = [self rowOfSender:sender];
     // Your code here
}
Borzh
  • 5,069
  • 2
  • 48
  • 64
0

CustomTableCell.h is a UITableViewCell:

@property (weak, nonatomic) IBOutlet UIButton *action1Button;
@property (weak, nonatomic) IBOutlet UIButton *action2Button;

MyVC.m after imports:

@interface MYTapGestureRecognizer : UITapGestureRecognizer
@property (nonatomic) NSInteger dataint;
@end

Inside "cellForRowAtIndexPath" in MyVC.m:

//CustomTableCell 
CustomTableCell *cell = (CustomTableCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];

//Set title buttons
[cell.action1Button setTitle:[NSString stringWithString:NSLocalizedString(@"action1", nil)] forState:UIControlStateNormal];
[cell.action2Button setTitle:[NSString stringWithString:NSLocalizedString(@"action2", nil)] forState:UIControlStateNormal];

//Set visibility buttons
[cell.action1Button setHidden:FALSE];
[cell.action2Button setHidden:FALSE];

//Do 1 action
[cell.action1Button addTarget:self action:@selector(do1Action :) forControlEvents:UIControlEventTouchUpInside];

//Do 2 action
MYTapGestureRecognizer *action2Tap = [[MYTapGestureRecognizer alloc] initWithTarget:self action:@selector(do2Action :)];
cancelTap.numberOfTapsRequired = 1;
cancelTap.dataint = indexPath.row;
[cell.action2Button setUserInteractionEnabled:YES];
[cell.action2Button addGestureRecognizer:action2Tap];

MyVC.m:

-(void)do1Action :(id)sender{
//do some action that is not necessary fr data
}

-(void)do2Action :(UITapGestureRecognizer *)tapRecognizer{
MYTapGestureRecognizer *tap = (MYTapGestureRecognizer *)tapRecognizer;
numberTag = tap.dataint;
FriendRequest *fr = [_list objectAtIndex:numberTag];

//connect with a WS o do some action with fr data

//actualize list in tableView
 [self.myTableView reloadData];
}
Mer
  • 41
  • 4
-1
cell.show.tag=indexPath.row;
     [cell.show addTarget:self action:@selector(showdata:) forControlEvents:UIControlEventTouchUpInside];

-(IBAction)showdata:(id)sender
{
    UIButton *button = (UIButton *)sender;

    UIStoryboard *storyBoard;
    storyBoard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
    SecondViewController *detailView = [storyBoard instantiateViewControllerWithIdentifier:@"SecondViewController"];

    detailView.string=[NSString stringWithFormat:@"%@",[_array objectAtIndex:button.tag]];

    [self presentViewController:detailView animated:YES completion:nil];

}
FelixSFD
  • 6,052
  • 10
  • 43
  • 117