1

I have a table of dynamic content. In the prototype cell I have a button. In that button's action method, how do I then determine which row's button was pressed?

(I know that, for example, in a segue method one can determine which row has been pressed by querying the selected path of the table view, but in this case, no row has actually been selected.)

Neil Coffey
  • 21,615
  • 7
  • 62
  • 83

3 Answers3

2

There are of course ways of doing this using tags, or storing an indexPath in your cell etc.

I prefer to use something similar to the following, which I think is cleaner than the above. You could add this to a category UITableViewController.

- (NSIndexPath *)indexPathForCellSubview:(UIView *)subview
{
    if (subview) {
        UITableViewCell *cell = [self tableViewCellForCellSubview:subview];
        return [self.tableView indexPathForCell:cell];
    }
    return nil;
}

- (UITableViewCell *)tableViewCellForCellSubview:(UIView *)subview
{
    if (subview) {
        UIView *superView = subview.superview;
        while (true) {
            if (superView) {
                if ([superView isKindOfClass:[UITableViewCell class]]) {
                    return (UITableViewCell *)superView;
                }
                superView = [superView superview];
            } else {
                return nil;
            }
        }
    } else {
        return nil;
    }
}

Edit with suggestion from @staticVoidMan:

More concise implementation:

- (UITableViewCell *)tableViewCellForCellSubview:(UIView *)subview
{
    UIView *checkSuperview = subview.superview;

    while (checkSuperview) {
        if ([checkSuperview isKindOfClass:[UITableViewCell class]]) {
            break;
        } else {
            checkSuperview = checkSuperview.superview;
        }
    }
    return (UITableViewCell *)checkSuperview;
}
JoeFryer
  • 2,751
  • 1
  • 18
  • 23
  • Superview trickery! I would argue that this is a hack, which is the opposite of clean code. – CrimsonChris May 29 '14 at 14:30
  • and... will crash in iOS6 (_if anyone cares about it_) – staticVoidMan May 29 '14 at 14:31
  • Yes, it is superview trickery. I would argue that this is less hack-ey than using tags or associated objects though. – JoeFryer May 29 '14 at 14:49
  • @staticVoidMan I was going to combine this method of finding the superview and set the tag on the table cell. In principle this seems to work fine, but what's the problem with it crashing iOS6 exactly? – Neil Coffey May 29 '14 at 15:45
  • @NeilCoffey : in iOS6, the immediate `superview` of the `button` is a `UITableViewCell` subclass. in iOS7, (_they changed things a bit_) the immediate `superview` of the `button` is a private class called `UITableViewCellScrollView` and the `superview` of this is the required `UITableViewCell`. So when you do `button.superview.superview` (_in iOS6_), it returns `UITableView` and crashes when you try to implement a `UITableViewCell` method on it (_i haven't tested his method but when I see `superview`, I get over cautious, but it looks like he's handling this case... does it crash in iOS6?_) – staticVoidMan May 29 '14 at 16:10
  • I've just tried it - this does work on iOS 6. – JoeFryer May 29 '14 at 16:23
  • @JoeFryer : great, so you did handle it via `if ([superView isKindOfClass:[UITableViewCell class]]) { return (UITableViewCell *)superView;}`. nice work – staticVoidMan May 29 '14 at 16:26
  • Yep - it keeps going up the view hierarchy until it finds a `UITableViewCell`. – JoeFryer May 29 '14 at 16:32
  • Ah OK this is what I'm doing: traversing up the hierarchy looking for a view of the right class. So it looks like I should be good! – Neil Coffey May 29 '14 at 16:36
  • Just for info, the immediate superview would actually be (should be) a `UITableViewCellContentView` in both cases; the next superview is a `UITableViewCellScrollView` on iOS 7 (this is missing on iOS 6), then a 'UITableViewCell'. – JoeFryer May 29 '14 at 16:41
  • @JoeFryer : yeah, you're right. `UITableViewCellContentView` and then yada yada. But... that was the point. I only saw `superview` and didn't see the part where you handled the cases. Anyways... might i suggest an improvement? (_just for kicks_) – staticVoidMan May 29 '14 at 16:46
  • Yeah, I just thought I'd note that here for anyone reading. Of course, please do! – JoeFryer May 29 '14 at 16:51
  • @JoeFryer : this? [pastebin](http://pastebin.com/HdnsYfAL) ... the `while (true)` is potentially infinite (_if... you send a `UIView` that does not have a `UITableViewCell` `superview` in it's upper hierarchy. **But**... what're the chances, right? hehe_) – staticVoidMan May 29 '14 at 16:52
  • It will still break the loop once it reaches the top view in the hierarchy :) I like yours though, it's more concise. Edited. – JoeFryer May 29 '14 at 17:04
  • @JoeFryer : well... i'd still prefer my _answer_ :P but your approach was pretty interesting. anyways... – staticVoidMan May 29 '14 at 17:14
1

You could use tags, associated objects, or superview trickery.

Here's a question that has all three answers.

Tags

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellIdentifier = @"MySpecialCell";
    MySpecialCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (!cell) {
        cell = [[MySpecialCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
        cell.mySpecialTextView.delegate = self;
    }
    cell.mySpecialTextView.tag = indexPath.row;
    return cell;
}

- (void)textViewDidChange:(UITextView *)textView
    int rowOfTextViewThatJustChanged = textView.tag;
}

Associated Objects

static NSString *const kIndexPathKey = @"indexPathKey";

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellIdentifier = @"MultiSelectCell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
        UISwitch *switchView = [[UISwitch alloc] init];
        [switchView addTarget:self action:@selector(switchValueChanged:) forControlEvents:UIControlEventValueChanged];
        cell.accessoryView = switchView;
    }

    UISwitch *switchView = (id)cell.accessoryView;
    [self setIndexPath:indexPath onSwitch:switchView];
    switchView.on = [self isIndexPathSelected:indexPath];

    id item = [self itemAtIndexPath:indexPath];
    cell.textLabel.text = safePerformSelector(item, self.itemDescriptionSelector);
    return cell;
}

- (void)switchValueChanged:(UISwitch *)sender {
    NSIndexPath *indexPath = [self indexPathOfSwitch:sender];
    [self setRowSelected:sender.isOn atIndexPath:indexPath];
    [self.delegate didSelectItem:[self itemAtIndexPath:indexPath] atIndexPath:indexPath selected:sender.isOn sender:self];
}

- (void)setIndexPath:(NSIndexPath *)indexPath onSwitch:(UISwitch *)switchView {
    [switchView setAssociatedObject:indexPath forKey:kIndexPathKey];
}

- (NSIndexPath *)indexPathOfSwitch:(UISwitch *)switchView {
    return [switchView associatedObjectForKey:kIndexPathKey];
}


@implementation NSObject (AssociatedObjects)

- (void)setAssociatedObject:(id)object forKey:(NSString *const)key {
    objc_setAssociatedObject(self, (__bridge const void *)(key), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (id)associatedObjectForKey:(NSString *const)key {
    return objc_getAssociatedObject(self, (__bridge const void *)(key));
}

@end
Community
  • 1
  • 1
CrimsonChris
  • 4,651
  • 2
  • 19
  • 30
1

Best approach:

-(void)buttonMethod:(UIButton *)sender event:(UIEvent *)event
{
    NSSet *touches = [event allTouches];
    UITouch *touch = [touches anyObject];
    CGPoint pointCurrent = [touch locationInView:self.tableView];
    NSIndexPath *indexPath = [self.tableView indexPathForItemAtPoint:pointCurrent];

    //...
}

PS: Provided you have done something like:

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

    [cell.someButton addTarget:self
                        action:@selector(buttonMethod:event:)
              forControlEvents:UIControlEventTouchUpInside];

    //...
}
staticVoidMan
  • 19,275
  • 6
  • 69
  • 98
  • Best is subjective, this approach has failed for me in the past with resizing tableviews. – CrimsonChris May 29 '14 at 14:47
  • @CrimsonChris : subjective indeed but i have never faced an issue with this. What was the exact case in which it failed for you? (_just curious_) – staticVoidMan May 29 '14 at 14:51
  • When using a table view that automatically scrolls the cell you just touched to the center. – CrimsonChris May 29 '14 at 14:55
  • @CrimsonChris : hm... i'll have to see this for myself. maybe you were scrolling before catching the point (_not sure but i'll test this_) – staticVoidMan May 29 '14 at 14:58
  • That's exactly what was happening. Didn't want to change that behavior. Went with associated indexpaths, hasn't let me down yet! – CrimsonChris May 29 '14 at 15:02