217

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.

Imanou Petit
  • 89,880
  • 29
  • 256
  • 218
rein
  • 32,967
  • 23
  • 82
  • 106
  • I don't see any problem with using your tag solution. The cells are reused, so it makes sense to set the tag to the row index the way you are doing it here. I find this a much more elegant solution than converting the touch location to a row index, as suggested below. – Erik van der Neut May 05 '15 at 10:10

26 Answers26

401

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)
    {
     ...
    }
}
iwasrobbed
  • 46,496
  • 21
  • 150
  • 195
Vladimir
  • 170,431
  • 36
  • 387
  • 313
  • Yeah this is what I settled on (see my edit). I agree with you that it's not optimal. – rein Nov 26 '09 at 10:35
  • This approach is not too reliable, as you will have to rely on the assumption of how 'nested' the button is within the `UITableViewCell` – ohhorob May 14 '10 at 00:35
  • 2
    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 – Vladimir May 14 '10 at 04:30
  • 1
    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. – raidfive May 21 '10 at 02:57
  • 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... – Vladimir May 21 '10 at 07:33
  • 3
    This will reliably pick out the cell that owns the button: UIView *view = button; while (![view isKindOfClass:[UITableViewCell class]]){ view = [view superview]} – Jacob Lyles Aug 11 '10 at 20:42
  • This does not work for me with iOS 7. IT randomly picks the wrong cell. – Neil Sep 20 '13 at 13:10
  • @Vladimir, what is a scenario when `indexPath != nil` is false? – Andrew Oct 07 '13 at 18:09
  • indexPath should be nil only if 'buttonPosition' point was out of bounds of any row, so I think that should not happen in practice – Vladimir Oct 08 '13 at 06:53
  • @Vladimir: Wonderful! Thanks. People should not forget the "self" in "self.tableView"... This only got me stuck for 2 hours. – Armand Nov 01 '13 at 00:17
  • It doesn't for me in iOS 7 when the section is used. Any suggestion? – Bagusflyer Feb 15 '14 at 02:19
  • 1
    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 – bandw Nov 30 '14 at 19:43
  • @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 – Vladimir Dec 01 '14 at 13:39
  • This code works for me in iOS 7 and iOS 8 too. Great Nice work !! – Kirti Nikam Jan 11 '15 at 18:27
  • 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 :-) ) – clearlight Feb 03 '15 at 23:42
48

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];
...

}
Cocoanut
  • 664
  • 1
  • 5
  • 7
  • 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 – software evolved May 09 '11 at 16: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. – Jon Schneider Jun 16 '13 at 03:19
  • 29
    This is so very, very wrong and is broken in newer versions of the OS. Don't walk superview trees you don't own. – Kenrik March Jul 18 '13 at 23:12
  • This worked for me with a prototype cell in a storyboard by adding a third call to superview: [[[sender superview] superview] superview] – Sam Brodkin Sep 27 '13 at 21:17
  • 2
    This was working for me under iOS 6, but is broken in iOS 7. It appears that @KenrikMarch has a valid point! – Jon Schneider Oct 05 '13 at 02:05
  • 3
    in iOS 7 it's 1 more step up the superview. e.g. [[[sender superview] superview] superView]; – CW0007007 Jan 21 '14 at 12:19
  • 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 – sudoExclaimationExclaimation Aug 09 '16 at 04:00
43

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];
    ...
}
Iulian Onofrei
  • 9,188
  • 10
  • 67
  • 113
Chris Schwerdt
  • 1,141
  • 9
  • 9
8

With Swift 4.2 and iOS 12, you can choose one the 5 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
    }

}

#5. Using UITableViewCell's accessoryType and UITableViewDelegate's tableView(_:accessoryButtonTappedForRowWith:)

If your button is a UITableViewCell's standard accessory control, any tap on it will trigger a call to UITableViewDelegate's tableView(_:accessoryButtonTappedForRowWith:), allowing you to get the related index path.

import UIKit

private class CustomCell: UITableViewCell {

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        accessoryType = .detailButton
    }

    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
    }

    override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
        print(indexPath)
    }

}
Imanou Petit
  • 89,880
  • 29
  • 256
  • 218
6

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...
}
Iulian Onofrei
  • 9,188
  • 10
  • 67
  • 113
Alpinista
  • 3,751
  • 5
  • 35
  • 46
  • 5
    It's not clear in this example where you get the 'event' object from. – Nick Ludlam Jan 27 '10 at 13:43
  • This is the solution I went with. Using tags is unpredictable when adding/removing rows since their indexes change. Also, – raidfive May 21 '10 at 02:54
  • @NickLudlam: probably the method name is not `buttonPressedAction:` but `buttonPressedAction:forEvent:`. – KPM Oct 23 '12 at 04:43
5

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
    }
Paras Joshi
  • 20,427
  • 11
  • 57
  • 70
magno cardona
  • 71
  • 1
  • 3
5

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"

Adrian
  • 16,233
  • 18
  • 112
  • 180
dennis
  • 143
  • 1
  • 12
5
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")
    }
Iulian Onofrei
  • 9,188
  • 10
  • 67
  • 113
Ankit Bansal
  • 51
  • 1
  • 3
3

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;
ACBurk
  • 4,418
  • 4
  • 34
  • 50
  • 5
    This approach is completely broken for tables with sections. – ohhorob May 14 '10 at 00:36
  • no, you could just use some simple function to put the section in the tag as well. – ACBurk May 14 '10 at 06:04
  • 2
    `tag` is an integer. seems a bit clumsy to be encoding/decoding index paths into view tags. – ohhorob May 14 '10 at 16: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. – ACBurk May 19 '10 at 19:15
3

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
}
Paras Joshi
  • 20,427
  • 11
  • 57
  • 70
Eld
  • 974
  • 6
  • 9
2

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);
    }
}
brian.clear
  • 5,277
  • 2
  • 41
  • 62
2
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; //:)
}
mmmanishs
  • 601
  • 6
  • 10
  • This is slightly buggy because a cell's indexPath can change, if you call deleteRowsAtIndexPaths. – John Gibb Jun 18 '14 at 20:14
  • deleteRowsAtIndexPaths will cause cellForRowAtIndexPath to get called again. Then buttons will have new correct indexPaths. – mmmanishs Oct 02 '14 at 22:04
1

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];
...

}
user366584
  • 1,016
  • 1
  • 16
  • 34
0

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.

Paras Joshi
  • 20,427
  • 11
  • 57
  • 70
0

I always use tags.

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

Paras Joshi
  • 20,427
  • 11
  • 57
  • 70
Chris
  • 2,727
  • 2
  • 27
  • 28
  • 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? – rein Nov 26 '09 at 10:18
0

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)
}
Nir Levy
  • 4,613
  • 2
  • 34
  • 47
  • How would I tag the controls if I had multiple controls on a single cell? – rein Nov 26 '09 at 10:20
  • 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. – rein Nov 26 '09 at 10:21
  • 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) – Nir Levy Nov 27 '09 at 08:51
0

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.

0

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.

Michael Morrison
  • 1,323
  • 1
  • 18
  • 30
0
// 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.

ohhorob
  • 11,695
  • 7
  • 41
  • 50
0

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);
    }
Sangram Shivankar
  • 3,535
  • 3
  • 26
  • 38
Gaurav
  • 553
  • 5
  • 9
0

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)

}
Lukesivi
  • 2,206
  • 4
  • 25
  • 43
0

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)
}
Rutger Huijsmans
  • 2,330
  • 2
  • 30
  • 67
0

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.

erkanyildiz
  • 13,044
  • 6
  • 50
  • 73
0

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

}
}
rajesh
  • 592
  • 4
  • 13
0

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)
    }
}
Ben Ong
  • 913
  • 1
  • 10
  • 25
0

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];
}
software evolved
  • 4,314
  • 35
  • 45