By rein


2009-11-26 09:57:01 8 Comments

I have a UITableView with 5 UITableViewCells. Each cell contains a UIButton which is set up as follows:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
     NSString *identifier = @"identifier";
     UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
     if (cell == nil) {
         cell = [[UITableView alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
         [cell autorelelase];

         UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(10, 5, 40, 20)];
         [button addTarget:self action:@selector(buttonPressedAction:) forControlEvents:UIControlEventTouchUpInside];
         [button setTag:1];
         [cell.contentView addSubview:button];

         [button release];
     }

     UIButton *button = (UIButton *)[cell viewWithTag:1];
     [button setTitle:@"Edit" forState:UIControlStateNormal];

     return cell;
}

My question is this: in the buttonPressedAction: method, how do I know which button has been pressed. I've considered using tags but I'm not sure this is the best route. I'd like to be able to somehow tag the indexPath onto the control.

- (void)buttonPressedAction:(id)sender
{
    UIButton *button = (UIButton *)sender;
    // how do I know which button sent this message?
    // processing button press for this row requires an indexPath. 
}

What's the standard way of doing this?

Edit:

I've kinda solved it by doing the following. I would still like to have an opinion whether this is the standard way of doing it or is there a better way?

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
     NSString *identifier = @"identifier";
     UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
     if (cell == nil) {
         cell = [[UITableView alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
         [cell autorelelase];

         UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(10, 5, 40, 20)];
         [button addTarget:self action:@selector(buttonPressedAction:) forControlEvents:UIControlEventTouchUpInside];
         [cell.contentView addSubview:button];

         [button release];
     }

     UIButton *button = (UIButton *)[cell.contentView.subviews objectAtIndex:0];
     [button setTag:indexPath.row];
     [button setTitle:@"Edit" forState:UIControlStateNormal];

     return cell;
}

- (void)buttonPressedAction:(id)sender
{
    UIButton *button = (UIButton *)sender;
    int row = button.tag;
}

What's important to note is that I can't set the tag in the creation of the cell since the cell might be dequeued instead. It feels very dirty. There must be a better way.

26 comments

@Imanou Petit 2018-08-07 21:46:33

With Swift 4.2 and iOS 12, you can choose one the following complete examples in order to solve your problem.


#1. Using UIView's convert(_:to:) and UITableview's indexPathForRow(at:)

import UIKit

private class CustomCell: UITableViewCell {

    let button = UIButton(type: .system)

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)

        button.setTitle("Tap", for: .normal)
        contentView.addSubview(button)

        button.translatesAutoresizingMaskIntoConstraints = false
        button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
        button.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
        button.topAnchor.constraint(equalToSystemSpacingBelow: contentView.topAnchor, multiplier: 1).isActive = true
        button.leadingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: contentView.leadingAnchor, multiplier: 1).isActive = true
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}
import UIKit

class TableViewController: UITableViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(CustomCell.self, forCellReuseIdentifier: "CustomCell")
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 3
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
        cell.button.addTarget(self, action: #selector(customCellButtonTapped), for: .touchUpInside)
        return cell
    }

    @objc func customCellButtonTapped(_ sender: UIButton) {
        let point = sender.convert(CGPoint.zero, to: tableView)
        guard let indexPath = tableView.indexPathForRow(at: point) else { return }
        print(indexPath)
    }

}

#2. Using UIView's convert(_:to:) and UITableview's indexPathForRow(at:) (alternative)

This is an alternative to the previous example where we pass nil to the target parameter in addTarget(_:action:for:). This way, if the first responder does not implement the action, it will be send to the next responder in the responder chain until until a proper implementation is found.

import UIKit

private class CustomCell: UITableViewCell {

    let button = UIButton(type: .system)

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)

        button.setTitle("Tap", for: .normal)
        button.addTarget(nil, action: #selector(TableViewController.customCellButtonTapped), for: .touchUpInside)
        contentView.addSubview(button)

        button.translatesAutoresizingMaskIntoConstraints = false
        button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
        button.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
        button.topAnchor.constraint(equalToSystemSpacingBelow: contentView.topAnchor, multiplier: 1).isActive = true
        button.leadingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: contentView.leadingAnchor, multiplier: 1).isActive = true
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}
import UIKit

class TableViewController: UITableViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(CustomCell.self, forCellReuseIdentifier: "CustomCell")
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 3
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
        return cell
    }

    @objc func customCellButtonTapped(_ sender: UIButton) {
        let point = sender.convert(CGPoint.zero, to: tableView)
        guard let indexPath = tableView.indexPathForRow(at: point) else { return }
        print(indexPath)
    }

}

#3. Using UITableview's indexPath(for:) and delegate pattern

In this example, we set the view controller as the delegate of the cell. When the cell's button is tapped, it triggers a call to the appropriate method of the delegate.

import UIKit

protocol CustomCellDelegate: AnyObject {
    func customCellButtonTapped(_ customCell: CustomCell)
}

class CustomCell: UITableViewCell {

    let button = UIButton(type: .system)
    weak var delegate: CustomCellDelegate?

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)

        button.setTitle("Tap", for: .normal)
        button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
        contentView.addSubview(button)

        button.translatesAutoresizingMaskIntoConstraints = false
        button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
        button.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
        button.topAnchor.constraint(equalToSystemSpacingBelow: contentView.topAnchor, multiplier: 1).isActive = true
        button.leadingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: contentView.leadingAnchor, multiplier: 1).isActive = true
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    @objc func buttonTapped(sender: UIButton) {
        delegate?.customCellButtonTapped(self)
    }

}
import UIKit

class TableViewController: UITableViewController, CustomCellDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(CustomCell.self, forCellReuseIdentifier: "CustomCell")
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 3
    }

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

    // MARK: - CustomCellDelegate

    func customCellButtonTapped(_ customCell: CustomCell) {
        guard let indexPath = tableView.indexPath(for: customCell) else { return }
        print(indexPath)
    }

}

#4. Using UITableview's indexPath(for:) and a closure for delegation

This is an alternative to the previous example where we use a closure instead of a protocol-delegate declaration to handle the button tap.

import UIKit

class CustomCell: UITableViewCell {

    let button = UIButton(type: .system)
    var buttontappedClosure: ((CustomCell) -> Void)?

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)

        button.setTitle("Tap", for: .normal)
        button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
        contentView.addSubview(button)

        button.translatesAutoresizingMaskIntoConstraints = false
        button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
        button.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
        button.topAnchor.constraint(equalToSystemSpacingBelow: contentView.topAnchor, multiplier: 1).isActive = true
        button.leadingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: contentView.leadingAnchor, multiplier: 1).isActive = true
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    @objc func buttonTapped(sender: UIButton) {
        buttontappedClosure?(self)
    }

}
import UIKit

class TableViewController: UITableViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(CustomCell.self, forCellReuseIdentifier: "CustomCell")
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 3
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
        cell.buttontappedClosure = { [weak tableView] cell in
            guard let indexPath = tableView?.indexPath(for: cell) else { return }
            print(indexPath)
        }
        return cell
    }

}

@Ben Ong 2017-07-24 10:21:30

I use a solution that subclass UIButton and I thought I should just share it here, codes in Swift:

class ButtonWithIndexPath : UIButton {
    var indexPath:IndexPath?
}

Then remember to update it's indexPath in cellForRow(at:)

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    let returnCell = tableView.dequeueReusableCell(withIdentifier: "cellWithButton", for: indexPath) as! cellWithButton
    ...
    returnCell.button.indexPath = IndexPath
    returnCell.button.addTarget(self, action:#selector(cellButtonPressed(_:)), for: .touchUpInside)

    return returnCell
}

So when responding to the button's event you can use it like

func cellButtonPressed(_ sender:UIButton) {
    if sender is ButtonWithIndexPath {
        let button = sender as! ButtonWithIndexPath
        print(button.indexPath)
    }
}

@erkanyildiz 2016-10-27 05:19:55

This problem has two parts:

1) Getting the index path of UITableViewCell which contains pressed UIButton

There are some suggestions like:

  • Updating UIButton's tag in cellForRowAtIndexPath: method using index path's row value. This is not an good solution as it requires updating tag continuously and it does not work with table views with more than one section.

  • Adding an NSIndexPath property to custom cell and updating it instead of UIButton's tag in cellForRowAtIndexPath: method. This solves multiple section problem but still not good as it requires updating always.

  • Keeping a weak refence to parent UITableView in the custom cell while creating it and using indexPathForCell: method to get the index path. Seems a little bit better, no need to update anything in cellForRowAtIndexPath: method, but still requires setting a weak reference when the custom cell is created.

  • Using cell's superView property to get a reference to parent UITableView. No need to add any properties to the custom cell, and no need to set/update anything on creation/later. But cell's superView depends on iOS implementation details. So it can not be used directly.

But this can be achieved using a simple loop, as we are sure the cell in question has to be in a UITableView:

UIView* view = self;
while (view && ![view isKindOfClass:UITableView.class])
    view = view.superview;
UITableView* parentTableView = (UITableView*)view;

So, these suggestions can be combined into a simple and safe custom cell method for getting the index path:

- (NSIndexPath *)indexPath
{
    UIView* view = self;

    while (view && ![view isKindOfClass:UITableView.class])
        view = view.superview;

    return [(UITableView*)view indexPathForCell:self];
}

From now on, this method can be used to detect which UIButton is pressed.

2) Informing other parties about button press event

After internally knowing which UIButton is pressed in which custom cell with exact index path, this information needs to be sent to other parties (most probably the view controller handling the UITableView). So, this button click event can be handled in a similar abstraction and logic level to didSelectRowAtIndexPath: method of UITableView delegate.

Two approaches can be used for this:

a) Delegation: custom cell can have a delegate property and can define a protocol. When button is pressed it just performs it's delegate methods on it's delegate property. But this delegate property needs to be set for each custom cell when they are created. As an alternative, custom cell can choose to perform its delegate methods on it's parent table view's delegate too.

b) Notification Center: custom cells can define a custom notification name and post this notification with the index path and parent table view information provided in userInfo object. No need to set anything for each cell, just adding an observer for the custom cell's notification is enough.

@Rutger Huijsmans 2016-10-17 12:10:18

Chris Schwerdt's solution but then in Swift worked for me:

@IBAction func rateButtonTapped(sender: UIButton) {
    let buttonPosition : CGPoint = sender.convertPoint(CGPointZero, toView: self.ratingTableView)
    let indexPath : NSIndexPath = self.ratingTableView.indexPathForRowAtPoint(buttonPosition)!

    print(sender.tag)
    print(indexPath.row)
}

@dennis 2014-12-11 19:29:54

To do (@Vladimir)'s answer is Swift:

var buttonPosition = sender.convertPoint(CGPointZero, toView: self.tableView)
var indexPath = self.tableView.indexPathForRowAtPoint(buttonPosition)!

Although checking for indexPath != nil gives me the finger..."NSIndexPath is not a subtype of NSString"

@Chris Schwerdt 2012-09-17 04:51:52

Here's how I do it. Simple and concise:

- (IBAction)buttonTappedAction:(id)sender
{
    CGPoint buttonPosition = [sender convertPoint:CGPointZero
                                           toView:self.tableView];
    NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:buttonPosition];
    ...

@Jakob W 2013-04-10 08:41:45

Even more simpler: use CGPointZero instead of CGPointMake(0, 0) ;-)

@Francisco Romero 2016-10-13 15:58:52

Easy to work with it. Further, easy to translate it to Swift 3. You are the best :)

@Rutger Huijsmans 2016-10-17 12:11:20

Translated this to Swift down below. Easiest solution I could find. Thanks Chris!

@Gaurav 2015-02-06 12:46:09

Note here i am using custom cell this code is perfectly working for me

 @IBAction func call(sender: UIButton)
    {
        var contentView = sender.superview;
        var cell = contentView?.superview as EmployeeListCustomCell
        if (!(cell.isKindOfClass(EmployeeListCustomCell)))
        {
            cell = (contentView?.superview)?.superview as EmployeeListCustomCell
        }

        let phone = cell.lblDescriptionText.text!
        //let phone = detailObject!.mobile!
        let url:NSURL = NSURL(string:"tel://"+phone)!;
        UIApplication.sharedApplication().openURL(url);
    }

@Lukesivi 2015-11-07 10:21:55

SWIFT 2 UPDATE

Here's how to find out which button was tapped + send data to another ViewController from that button's indexPath.row as I'm assuming that's the point for most!

@IBAction func yourButton(sender: AnyObject) {


     var position: CGPoint = sender.convertPoint(CGPointZero, toView: self.tableView)
        let indexPath = self.tableView.indexPathForRowAtPoint(position)
        let cell: UITableViewCell = tableView.cellForRowAtIndexPath(indexPath!)! as
        UITableViewCell
        print(indexPath?.row)
        print("Tap tap tap tap")

    }

For those who are using a ViewController class and added a tableView, I'm using a ViewController instead of a TableViewController so I manually added the tableView in order to access it.

Here is the code for passing data to another VC when tapping that button and passing the cell's indexPath.row

@IBAction func moreInfo(sender: AnyObject) {

    let yourOtherVC = self.storyboard!.instantiateViewControllerWithIdentifier("yourOtherVC") as! YourOtherVCVIewController



    var position: CGPoint = sender.convertPoint(CGPointZero, toView: self.tableView)
    let indexPath = self.tableView.indexPathForRowAtPoint(position)
    let cell: UITableViewCell = tableView.cellForRowAtIndexPath(indexPath!)! as
    UITableViewCell
    print(indexPath?.row)
    print("Button tapped")


    yourOtherVC.yourVarName = [self.otherVCVariable[indexPath!.row]]

    self.presentViewController(yourNewVC, animated: true, completion: nil)

}

@Ankit Bansal 2015-03-31 05:58:11

func buttonAction(sender:UIButton!)
    {
        var position: CGPoint = sender.convertPoint(CGPointZero, toView: self.tablevw)
       let indexPath = self.tablevw.indexPathForRowAtPoint(position)
       let cell: TableViewCell = tablevw.cellForRowAtIndexPath(indexPath!) as TableViewCell
        println(indexPath?.row)
        println("Button tapped")
    }

@mmmanishs 2013-10-19 08:06:31

A better way would be to subclass your button and add a indexPath property to it.

//Implement a subclass for UIButton.

@interface NewButton:UIButton
@property(nonatomic, strong) NSIndexPath *indexPath;


Make your button of type NewButton in the XIB or in the code whereever you are initializing them.

Then in the cellForRowAtIndexPath put the following line of code.

button.indexPath = indexPath;

return cell; //As usual



Now in your IBAction

-(IBAction)buttonClicked:(id)sender{
   NewButton *button = (NewButton *)sender;

//Now access the indexPath by buttons property..

   NSIndexPath *indexPath = button.indexPath; //:)
}

@John Gibb 2014-06-18 20:14:41

This is slightly buggy because a cell's indexPath can change, if you call deleteRowsAtIndexPaths.

@mmmanishs 2014-10-02 22:04:21

deleteRowsAtIndexPaths will cause cellForRowAtIndexPath to get called again. Then buttons will have new correct indexPaths.

@Jerome Chan Yeow Heong 2013-10-06 02:59:31

Subclass the button to store the required value, maybe create a protocol (ControlWithData or something). Set the value when you add the button to the table view cell. In your touch up event, see if the sender obeys the protocol and extract the data. I normally store a reference to the actual object that is rendered on the table view cell.

@piyush Bageria 2012-10-05 09:39:34

It's simple; make a custom cell and take a outlet of button

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
         NSString *identifier = @"identifier";
        customCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];

    cell.yourButton.tag = indexPath.Row;

- (void)buttonPressedAction:(id)sender

change id in above method to (UIButton *)

You can get the value that which button is being tapped by doing sender.tag.

@Chris 2009-11-26 10:13:24

I always use tags.

You need to subclass the UITableviewCell and handle the button press from there.

@rein 2009-11-26 10:18:40

I don't quite understand how. The tag property is set up during the cell creation - this cell is reusable for each row with the same identifier. This tag is specific to the control in a generic reusable cell. How can I use this tag to differentiate buttons in cells which were created in a generic way? Could you post some code?

@Eld 2009-11-26 17:21:40

Though I like the tag way... if you don't want to use tags for whatever reason, you could create a member NSArray of premade buttons:

NSArray* buttons ;

then create those buttons before rendering the tableView and push them into the array.

Then inside of the tableView:cellForRowAtIndexPath: function you can do:

UIButton* button = [buttons objectAtIndex:[indexPath row] ] ;
[cell.contentView addSubview:button];

Then in the buttonPressedAction: function, you can do

- (void)buttonPressedAction:(id)sender {
   UIButton* button = (UIButton*)sender ;
   int row = [buttons indexOfObject:button] ;
   // Do magic
}

@magno cardona 2012-12-09 22:11:36

How about sending the information like NSIndexPath in the UIButton using runtime injection.

1) You need runtime on the import

2) add static constant

3) add NSIndexPath to your button on runtime using:

(void)setMetaData:(id)target withObject:(id)newObj

4) on button press get metadata using:

(id)metaData:(id)target

Enjoy

    #import <objc/runtime.h>
    static char const * const kMetaDic = "kMetaDic";


    #pragma mark - Getters / Setters

- (id)metaData:(id)target {
    return objc_getAssociatedObject(target, kMetaDic);
}

- (void)setMetaData:(id)target withObject:(id)newObj {
    objc_setAssociatedObject(target, kMetaDic, newObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}



    #On the cell constructor
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
    ....
    cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    ....
    [btnSocial addTarget:self
                                   action:@selector(openComments:)
                         forControlEvents:UIControlEventTouchUpInside];

    #add the indexpath here or another object
    [self setMetaData:btnSocial withObject:indexPath];

    ....
    }



    #The action after button been press:

    - (IBAction)openComments:(UIButton*)sender{

        NSIndexPath *indexPath = [self metaData:sender];
        NSLog(@"indexPath: %d", indexPath.row);

        //Reuse your indexpath Now
    }

@Neil 2013-09-20 13:12:46

IF the table is rearranged or a row deleted then this won't work.

@Vladimir 2009-11-26 10:31:52

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)
    {
     ...
    }
}

@rein 2009-11-26 10:35:23

Yeah this is what I settled on (see my edit). I agree with you that it's not optimal.

@ohhorob 2010-05-14 00:35:10

This approach is not too reliable, as you will have to rely on the assumption of how 'nested' the button is within the UITableViewCell

@Vladimir 2010-05-14 04:30:44

But you add UIButton to UITableViewCell yourself so you must just be consistent with what you do when creating cell. Although this approach does not really look elegant I have to admit

@raidfive 2010-05-21 02:57:03

For the first solution, you will need to grab [[button superview] superview] since the first superview call will give you the contentView, and finally the second will give you the UITableViewCell. The second solution doesn't work well if you are adding/removing cells since it will invalidate the row index. Therefore, I went with the first solution as outlined and it worked perfect.

@Vladimir 2010-05-21 07:33:25

For the 1st solution-yes if you add button to the cell's contentview you will need to call superview twice (in my app I added button to the cell directly - that's why I called it once). In the second solution you will need to update cell's tag each time you reuse it - I mentioned it in the answer. 3rd solution may looks not so straightforward but apple used it in its sample so it may be the most correct one...

@Jacob Lyles 2010-08-11 20:42:01

This will reliably pick out the cell that owns the button: UIView *view = button; while (![view isKindOfClass:[UITableViewCell class]]){ view = [view superview]}

@mga 2012-04-20 04:20:41

Edit2 works wonders!

@Neil 2013-09-20 13:10:21

This does not work for me with iOS 7. IT randomly picks the wrong cell.

@Vladimir 2013-09-20 14:24:57

@Neil, code works fine for me on iOS 7

@Andrew 2013-10-07 18:07:46

@Neil, works for me with iOS 7 as well so far.

@Andrew 2013-10-07 18:09:48

@Vladimir, what is a scenario when indexPath != nil is false?

@Vladimir 2013-10-08 06:53:16

indexPath should be nil only if 'buttonPosition' point was out of bounds of any row, so I think that should not happen in practice

@Armand 2013-11-01 00:17:42

@Vladimir: Wonderful! Thanks. People should not forget the "self" in "self.tableView"... This only got me stuck for 2 hours.

@Bagusflyer 2014-02-15 02:19:09

It doesn't for me in iOS 7 when the section is used. Any suggestion?

@bandw 2014-11-30 19:43:34

There is a trap when using: [button addTarget:self action:@selector(checkButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; because addTarget:action:forControlEvents: will add multiple duplicated target and actions when you scrolling table, it won't remove the previous targets and actions, so the method checkButtonTapped: will be called many times when you click the button. You'd better remove the target and action before adding them

@Vladimir 2014-12-01 13:39:26

@bandw, good point, you should call addTarget:action: only once for each cell. In old-style code where cells were created manually that would be easy to do, now if cells are created automatically by table that could be a problem

@iKT 2015-01-11 18:27:31

This code works for me in iOS 7 and iOS 8 too. Great Nice work !!

@clearlight 2015-02-03 23:42:09

But be sure if you're deqeuing cell via an Interface Builder file (such as nib) on iOS 8 for example, that you set up the layout constraints, so that the left bounds of the button are within the cell (by default it may overhang the edge), or the coordinates derived from the button won't translate to a row using this method. I didn't realize my left constraint was out of bounds because the button image was within the cell, but I kept getting nil index paths. Figured it out from reading all the associated method references in the documentation ( (always a good idea, I'm told :-) )

@brian.clear 2012-09-11 14:58:41

TO HANDLE SECTIONS - I stored the NSIndexPath in a custom UITableViewCell

IN CLKIndexPricesHEADERTableViewCell.xib

IN IB Add UIButton to XIB - DONT add action!

Add outlet @property (retain, nonatomic) IBOutlet UIButton *buttonIndexSectionClose;

DO NOT CTRL+DRAG an action in IB(done in code below)

@interface CLKIndexPricesHEADERTableViewCell : UITableViewCell
...
@property (retain, nonatomic) IBOutlet UIButton *buttonIndexSectionClose;
@property (nonatomic, retain) NSIndexPath * indexPathForCell;
@end

In viewForHeaderInSection (should also work for cellForRow.... etc if you table has only 1 section)

- viewForHeaderInSection is called for each section 1...2...3
- get the cell CLKIndexPricesHEADERTableViewCell 
- getTableRowHEADER just does the normal dequeueReusableCellWithIdentifier
- STORE the indexPath IN the UITableView cell
- indexPath.section = (NSInteger)section
- indexPath.row = 0 always (we are only interested in sections)

- (UIView *) tableView:(UITableView *)tableView1 viewForHeaderInSection:(NSInteger)section {


    //Standard method for getting a UITableViewCell
    CLKIndexPricesHEADERTableViewCell * cellHEADER = [self getTableRowHEADER];

...use the section to get data for your cell

...fill it in

   indexName        = ffaIndex.routeCode;
   indexPrice       = ffaIndex.indexValue;

   //

   [cellHEADER.buttonIndexSectionClose addTarget:self
                                          action:@selector(buttonDELETEINDEXPressedAction:forEvent:)
                                forControlEvents:UIControlEventTouchUpInside];


   cellHEADER.indexPathForCell = [NSIndexPath indexPathForRow:0 inSection:section];


    return cellHEADER;
}

USER presses DELETE Button on a Section header and this calls

- (void)buttonDELETEINDEXPressedAction:(id)sender forEvent:(UIEvent *)event
{
    NSLog(@"%s", __PRETTY_FUNCTION__);


    UIView *  parent1 = [sender superview];   // UiTableViewCellContentView
    //UIView *myContentView = (UIView *)parent1;

    UIView *  parent2 = [parent1 superview];  // custom cell containing the content view
    //UIView *  parent3 = [parent2 superview];  // UITableView containing the cell
    //UIView *  parent4 = [parent3 superview];  // UIView containing the table


    if([parent2 isMemberOfClass:[CLKIndexPricesHEADERTableViewCell class]]){
        CLKIndexPricesHEADERTableViewCell *myTableCell = (CLKIndexPricesHEADERTableViewCell *)parent2;

        //UITableView *myTable = (UITableView *)parent3;
        //UIView *mainView = (UIView *)parent4;

        NSLog(@"%s indexPath.section,row[%d,%d]", __PRETTY_FUNCTION__, myTableCell.indexPathForCell.section,myTableCell.indexPathForCell.row);

        NSString *key = [self.sortedKeysArray objectAtIndex:myTableCell.indexPathForCell.section];
        if(key){
            NSLog(@"%s DELETE object at key:%@", __PRETTY_FUNCTION__,key);
            self.keyForSectionIndexToDelete = key;
            self.sectionIndexToDelete = myTableCell.indexPathForCell.section;

            UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Remove Index"
                                                                message:@"Are you sure"
                                                               delegate:self
                                                      cancelButtonTitle:@"No"
                                                      otherButtonTitles:@"Yes", nil];
            alertView.tag = kALERTVIEW_REMOVE_ONE_INDEX;
            [alertView show];
            [alertView release];
            //------
        }else{
            NSLog(@"ERROR: [%s] key is nil for section:%d", __PRETTY_FUNCTION__,myTableCell.indexPathForCell.section);
        }

    }else{
        NSLog(@"ERROR: [%s] CLKIndexPricesHEADERTableViewCell not found", __PRETTY_FUNCTION__);
    }
}

In this example I added a Delete button so should show UIAlertView to confirm it

I store the section and key into the dictionary storing info about the section in a ivar in the VC

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
   if(alertView.tag == kALERTVIEW_REMOVE_ONE_INDEX){
        if(buttonIndex==0){
            //NO
            NSLog(@"[%s] BUTTON:%d", __PRETTY_FUNCTION__,buttonIndex);
            //do nothing
        }
        else if(buttonIndex==1){
            //YES
            NSLog(@"[%s] BUTTON:%d", __PRETTY_FUNCTION__,buttonIndex);
            if(self.keyForSectionIndexToDelete != nil){

                //Remove the section by key
                [self.indexPricesDictionary removeObjectForKey:self.keyForSectionIndexToDelete];

                //sort the keys so sections appear alphabetically/numbericsearch (minus the one we just removed)
                [self updateTheSortedKeysArray];                

                //Delete the section from the table using animation
                [self.tableView beginUpdates];

                [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:self.sectionIndexToDelete]
                              withRowAnimation:UITableViewRowAnimationAutomatic];
                [self.tableView endUpdates];

                //required to trigger refresh of myTableCell.indexPathForCell else old values in UITableViewCells
                [self.tableView reloadData];
            }else{
                NSLog(@"ERROR: [%s] OBJECT is nil", __PRETTY_FUNCTION__);
            }
        }
        else {
            NSLog(@"ERROR: [%s] UNHANDLED BUTTON:%d", __PRETTY_FUNCTION__,buttonIndex);
        }
    }else {
        NSLog(@"ERROR: [%s] unhandled ALERTVIEW TAG:%d", __PRETTY_FUNCTION__,alertView.tag);
    }
}

@software evolved 2011-05-09 16:53:42

A slight variation on Cocoanuts answer (that helped me solve this) when the button was in the footer of a table (which prevents you from finding the 'clicked cell':

-(IBAction) buttonAction:(id)sender;
{
    id parent1 = [sender superview];   // UiTableViewCellContentView
    id parent2 = [parent1 superview];  // custom cell containing the content view
    id parent3 = [parent2 superview];  // UITableView containing the cell
    id parent4 = [parent3 superview];  // UIView containing the table

    UIView *myContentView = (UIView *)parent1;
    UITableViewCell *myTableCell = (UITableViewCell *)parent2;
    UITableView *myTable = (UITableView *)parent3;
    UIView *mainView = (UIView *)parent4;

    CGRect footerViewRect = myTableCell.frame;
    CGRect rect3 = [myTable convertRect:footerViewRect toView:mainView];    

    [cc doSomethingOnScreenAtY:rect3.origin.y];
}

@user366584 2011-03-07 12:11:17

It works for me aswell, Thanks @Cocoanut

I found the method of using the superview's superview to obtain a reference to the cell's indexPath worked perfectly. Thanks to iphonedevbook.com (macnsmith) for the tip link text

-(void)buttonPressed:(id)sender {
 UITableViewCell *clickedCell = (UITableViewCell *)[[sender superview] superview];
 NSIndexPath *clickedButtonPath = [self.tableView indexPathForCell:clickedCell];
...

}

@rajesh 2010-12-07 10:02:52

create an nsmutable array and put all button in that array usint[array addObject:yourButton];

in the button press method

-

 (void)buttonPressedAction:(id)sender
{
    UIButton *button = (UIButton *)sender;

for(int i=0;i<[yourArray count];i++){

if([buton isEqual:[yourArray objectAtIndex:i]]){

//here write wat u need to do

}
}

@ohhorob 2010-05-14 00:28:02

// how do I know which button sent this message?
// processing button press for this row requires an indexPath.

Pretty straightforward actually:

- (void)buttonPressedAction:(id)sender
{
    UIButton *button = (UIButton *)sender;
    CGPoint rowButtonCenterInTableView = [[rowButton superview] convertPoint:rowButton.center toView:self.tableView];
    NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:rowButtonCenterInTableView];
    MyTableViewItem *rowItem = [self.itemsArray objectAtIndex:indexPath.row];
    // Now you're good to go.. do what the intention of the button is, but with
    // the context of the "row item" that the button belongs to
    [self performFooWithItem:rowItem];
}

Working well for me :P

if you want to adjust your target-action setup, you can include the event parameter in the method, and then use the touches of that event to resolve the coordinates of the touch. The coordinates still need to be resolved in the touch view bounds, but that may seem easier for some people.

@Michael Morrison 2010-04-22 22:48:41

Am I missing something? Can't you just use sender to identify the button. Sender will give you info like this:

<UIButton: 0x4b95c10; frame = (246 26; 30 30); opaque = NO; tag = 104; layer = <CALayer: 0x4b95be0>>

Then if you want to change the properties of the button, say the background image you just tell sender:

[sender setBackgroundImage:[UIImage imageNamed:@"new-image.png"] forState:UIControlStateNormal];

If you need the tag then ACBurk's method is fine.

@ohhorob 2010-05-14 00:33:58

They're looking for their "object" that the button relates to

@Cocoanut 2010-01-06 02:54:37

I found the method of using the superview's superview to obtain a reference to the cell's indexPath worked perfectly. Thanks to iphonedevbook.com (macnsmith) for the tip link text

-(void)buttonPressed:(id)sender {
 UITableViewCell *clickedCell = (UITableViewCell *)[[sender superview] superview];
 NSIndexPath *clickedButtonPath = [self.tableView indexPathForCell:clickedCell];
...

}

@software evolved 2011-05-09 16:48:19

Cocoanut, your code fragment pointed me in the right direction for my own variation on this problem. Thanks! In case anyone else needs it, my special case was that the button was in a custom cell that was being displayed as part of the footer. I'll add code below

@Jon Schneider 2013-06-16 03:19:48

If you (Stackoverflow reader) try this and it doesn't work for you, check whether in your implementation your UIButton is actually the grandchild of your UITableViewCell. In my implementation, my UIButton was a direct child of my UITableViewCell, so I needed to take out one of the "superview"s in Cocoanut's code, and then it worked.

@Kenrik March 2013-07-18 23:12:59

This is so very, very wrong and is broken in newer versions of the OS. Don't walk superview trees you don't own.

@Sam Brodkin 2013-09-27 21:17:31

This worked for me with a prototype cell in a storyboard by adding a third call to superview: [[[sender superview] superview] superview]

@Jon Schneider 2013-10-05 02:05:50

This was working for me under iOS 6, but is broken in iOS 7. It appears that @KenrikMarch has a valid point!

@CW0007007 2014-01-21 12:19:15

in iOS 7 it's 1 more step up the superview. e.g. [[[sender superview] superview] superView];

@Pranoy C 2016-08-09 04:00:39

this is not perfect solution and is bound to break everytime the view hierarchy changes. good luck changing it everytime apple changes the hierarchy just like they did with the swipe to delete structure in iOS 7. @Vladimir solution is the correct one

@Alpinista 2009-12-10 23:32:15

Found a nice solution to this problem elsewhere, no messing around with tags on the button:

- (void)buttonPressedAction:(id)sender {

NSSet *touches = [event allTouches];
UITouch *touch = [touches anyObject];
CGPoint currentTouchPosition = [touch locationInView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint: currentTouchPosition];

do stuff with the indexPath...
}

@Nick Ludlam 2010-01-27 13:43:11

It's not clear in this example where you get the 'event' object from.

@raidfive 2010-05-21 02:54:56

This is the solution I went with. Using tags is unpredictable when adding/removing rows since their indexes change. Also,

@KPM 2012-10-23 04:43:39

@NickLudlam: probably the method name is not buttonPressedAction: but buttonPressedAction:forEvent:.

@Nir Levy 2009-11-26 10:16:46

you can use the tag pattern:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
     NSString *identifier = @"identifier";
     UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
     if (cell == nil) {
         cell = [[UITableView alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
         [cell autorelelase];

         UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(10, 5, 40, 20)];
         [button addTarget:self action:@selector(buttonPressedAction:) forControlEvents:UIControlEventTouchUpInside];
         [button setTag:[indexPath row]]; //use the row as the current tag
         [cell.contentView addSubview:button];

         [button release];
     }

     UIButton *button = (UIButton *)[cell viewWithTag:[indexPath row]]; //use [indexPath row]
     [button setTitle:@"Edit" forState:UIControlStateNormal];

     return cell;
}

- (void)buttonPressedAction:(id)sender
{
    UIButton *button = (UIButton *)sender;
    //button.tag has the row number (you can convert it to indexPath)
}

@rein 2009-11-26 10:20:04

How would I tag the controls if I had multiple controls on a single cell?

@rein 2009-11-26 10:21:31

I'm not sure this would work - if the cell gets created for row #1 then it will get the tag 1. If it gets dequeued for row #3 then it will still have a tag of 1, not 3.

@Nir Levy 2009-11-27 08:51:06

guess you are right about the second comment. my bad. I think your best solution is to subclass UIButton, add another property or two of your own, and then set/get them in the appropriate cases (stick with the tag:1 you had in your code)

@ACBurk 2009-11-26 10:16:38

I would use the tag property like you said, setting the tag like so:

[button setTag:indexPath.row];

then getting the tag inside of the buttonPressedAction like so:

((UIButton *)sender).tag

Or

UIButton *button = (UIButton *)sender; 
button.tag;

@ohhorob 2010-05-14 00:36:45

This approach is completely broken for tables with sections.

@ACBurk 2010-05-14 06:04:13

no, you could just use some simple function to put the section in the tag as well.

@ohhorob 2010-05-14 16:08:33

tag is an integer. seems a bit clumsy to be encoding/decoding index paths into view tags.

@ACBurk 2010-05-19 19:15:08

That's correct, but it is a solution, though not one that I'd use if I had sections. All I was trying to say was that it could be done using this method, that it wasn't broken. A better, more complex version would determine the indexpath from the position of the button inside of the UITableView. However, since rein has said he only has five cells (without sections), it probably makes that method over complicated and your initial comment and this whole comment thread pointless.

Related Questions

Sponsored Content

9 Answered Questions

[SOLVED] How to detect tableView cell touched or clicked in swift

36 Answered Questions

[SOLVED] How can I disable the UITableView selection?

25 Answered Questions

1 Answered Questions

[SOLVED] The first UITableViewCell's accessary arrow is not showing

0 Answered Questions

Add radio button on cell in uitableview

3 Answered Questions

How to display JSON array on UITableView

1 Answered Questions

[SOLVED] Count number of values in dictionary object

  • 2012-12-13 09:14:32
  • MIrfan
  • 282 View
  • 0 Score
  • 1 Answer
  • Tags:   ios uitableview

1 Answered Questions

Scrollview is displayed in multiple tableview cells

3 Answered Questions

[SOLVED] How to change UIButton title which placed in prototype cell?

2 Answered Questions

[SOLVED] help on button.tag = indexPath.row

Sponsored Content