22

I have a table view where each cell has a button accessory view. The table is managed by a fetched results controller and is frequently reordered. I want to be able to press one of the buttons and obtain the index path of that button's table view cell. I've been trying to get this working for days by storing the row of the button in its tag, but when the table gets reordered, the row becomes incorrect and I keep failing at reordering the tags correctly. Any new ideas on how to keep track of the button's cell's index path?

wxactly
  • 2,400
  • 1
  • 26
  • 42
Jake
  • 733
  • 2
  • 7
  • 18
  • Possible duplicate of [Detecting which UIButton was pressed in a UITableView](http://stackoverflow.com/questions/1802707/detecting-which-uibutton-was-pressed-in-a-uitableview) – Lukesivi Nov 07 '15 at 21:21

8 Answers8

31

If you feel uncomfortable relying on button.superview, this method should be a little more robust than some of the other answers here:

UIButton *button = (UIButton *)sender;
CGRect buttonFrame = [button convertRect:button.bounds toView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:buttonFrame.origin];
wxactly
  • 2,400
  • 1
  • 26
  • 42
25

This stopped working with iOS 7; check out Mike Weller's answer instead

- (IBAction)clickedButton:(id)sender {
    UIButton *button = (UIButton *)sender;
    UITableViewCell *cell = (UITableViewCell *)button.superview;
    UITableView *tableView = (UITableView *)cell.superview;
    NSIndexPath *indexPath = [tableView indexPathForCell:cell];
}

Or shorter:

- (IBAction)clickedButton:(id)sender {
    NSIndexPath *indexPath = [(UITableView *)sender.superview.superview indexPathForCell:(UITableViewCell *)sender.superview];
}

Both are untested!

Community
  • 1
  • 1
Douwe Maan
  • 6,888
  • 2
  • 34
  • 35
  • Thanks so much! This fixed the issue and now I can finally move on :) For those who get here later and want to implement this, in the first example you need one less superview call to get the UITableViewCell and superview is all lowercase. Otherwise, everything is perfect (I haven't tried the shorter example). – Jake May 19 '10 at 15:41
  • You're right, edited my answer ;) My logic behind the 2 `.superviews` to get the `UITableViewCell`: the cell is the `superview` of the `accessoryView` which is the `superview` of the `UIButton`. But I guess I was mistaken. – Douwe Maan May 19 '10 at 15:57
  • This may work, but crawling up view hierarchies is a 'smell' to say the least. If `UITableViewCell`'s view structure changes, your app breaks. And this has happened in the past. The button and its handler should exist on a custom cell subclass. This cell class can then provide a delegate or block callback interface so that your view controller can hook into the button event that way and has full access to the appropriate context. – Mike Weller Apr 10 '13 at 09:59
  • Please see my answer for more details. – Mike Weller Apr 10 '13 at 10:16
  • 7
    @MikeWeller Smell confirmed. This breaks in iOS7. cell.superView now returns a `UITableViewWrapperView` rather than the tableView itself. – memmons Sep 14 '13 at 20:40
  • Yup, my answer was terrible in retrospect. I've linked to Mike Weller's. – Douwe Maan Nov 13 '13 at 22:38
21

Crawling up view hierarchies with .superview (like all of the existing answers demonstrate) is a really bad way to do things. If UITableViewCell's structure changes (which has happened before) your app will break. Seeing .superview.superview in your code should set off alarm bells.

The button and its handler should be added to a custom UITableViewCell subclass and layed out there. That's where it belongs.

The cell subclass can then delegate out the button event through a standard delegate interface, or a block. You should aim for something like this:

- (UITableViewCell *)cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    MyCustomCell *cell = ...;

    // ...

    cell.onButtonTapped = ^{
        [self buttonSelectedAtIndexPath:indexPath];
    }

    // OR

    cell.delegate = self;

    // ...
}

(Note: if you go the block route, you will need to use a __weak self reference to prevent retain cycles, but I thought that would clutter up the example).

If you take the delegate route you would then have this delegate method to implement:

- (void)cellButtonPressed:(UITableViewCell *)cell
{
    NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
    // ...
}

Your code now has full access to the appropriate context when it handles the event.

Implementing this interface on your cell class should be straightforward.

Mike Weller
  • 45,401
  • 15
  • 131
  • 151
9

I don't know why I need to call the method superview twice to get the UITableViewCell.

Update:

Thank for Qiulang, now I got it.

"That's because SDK now has added a private class called UITableViewCellContentView for UITableViewCell, which is button's superview now." – Qiulang

UIButton *button = (UIButton *)sender;
UITableViewCell *cell = (UITableViewCell *)button.superview.superview;
UITableView *curTableView = (UITableView *)cell.superview;
NSIndexPath *indexPath = [curTableView indexPathForCell:cell];
Jibeex
  • 5,509
  • 3
  • 27
  • 37
  • 3
    That's because SDK now has added a private class called UITableViewCellContentView for UITableViewCell, which is button's superview now. – Qiulang Aug 23 '12 at 09:24
6

I had this same issue also and built a simple recursive method that works no matter how many views deep you triggering control is.

-(NSIndexPath*)GetIndexPathFromSender:(id)sender{

    if(!sender) { return nil; }

    if([sender isKindOfClass:[UITableViewCell class]])
    {
        UITableViewCell *cell = sender;
        return [self.tableView indexPathForCell:cell];
    }

    return [self GetIndexPathFromSender:((UIView*)[sender superview])];
}


-(void)ButtonClicked:(id)sender{
    NSIndexPath *indexPath = [self GetIndexPathFromSender:sender];
}
1kmonkies
  • 361
  • 5
  • 9
  • Careful for infinite recursion... I would probably add an escape hatch at the top of that method to be safe: `if(!sender) { return nil; }` – wxactly Sep 27 '13 at 20:46
  • this getIndexPathFromSender method is Grrrrrreat! – Yup. Sep 25 '14 at 15:23
4

I have created one Method for getting indexPath, Hope this will help you.

Create Button Action (aMethod:) in cellForRowAtIndexPath

-(void) aMethod:(UIButton *)sender
{
    // Calling Magic Method which will return us indexPath.
    NSIndexPath *indexPath = [self getButtonIndexPath:sender];

    NSLog(@"IndexPath: %li", indexPath.row);
    NSLog(@"IndexRow: %li", indexPath.section);
}

// Here is the Magic Method for getting button's indexPath

-(NSIndexPath *) getButtonIndexPath:(UIButton *) button
{
    CGRect buttonFrame = [button convertRect:button.bounds toView:groupTable];
    return [groupTable indexPathForRowAtPoint:buttonFrame.origin];
}
Gaurav Singla
  • 2,271
  • 26
  • 43
3

Use this Perfect working for me.

CGPoint center= [sender center];
CGPoint rootViewPoint = [[sender superview] convertPoint:center toView:_tableView1];
NSIndexPath *indexPath = [_tableView1 indexPathForRowAtPoint:rootViewPoint];
NSLog(@"%@",indexPath);
Yogesh Kumar
  • 289
  • 4
  • 4
2

SWIFT 2 UPDATE

Here's how to find out which button was tapped

 @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")

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