Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best practice: handling actions from buttons inside tableview cells?

I have a "contact list" table view with "contact" cells that contain an email button that, when tapped, should present an email composer with the email address of that contact.

What is the best way to associate the UIButton with the "contact" instance of that cell?

I’ve created answers for the two approaches that come to mind – but which I don’t really find satisfactory. Which do you prefer, or much better still, suggest better ones!

like image 555
Yang Meyer Avatar asked Apr 01 '13 14:04

Yang Meyer


People also ask

How do you get the indexPath row when a button in a cell is tapped?

add an 'indexPath` property to the custom table cell. initialize it in cellForRowAtIndexPath. move the tap handler from the view controller to the cell implementation. use the delegation pattern to notify the view controller about the tap event, passing the index path.

Is it possible to add UITableVIew within a UITableViewCell?

yes it is possible, I added the UITableVIew within the UITableView cell .. :) no need to add tableview cell in xib file - just subclass the UITableviewCell and use the code below, a cell will be created programatically.


5 Answers

Approach 1:

Determine the cell, and thence the index path, by traversing the cell’s view hierarchy from the button.

// ContactListViewController.m

- (IBAction)emailContact:(id)sender {
    YMContact *contact = [self contactFromContactButton:sender];
    // present composer with `contact`...
}

- (YMContact *)contactFromContactButton:(UIView *)contactButton {
    UIView *aSuperview = [contactButton superview];
    while (![aSuperview isKindOfClass:[UITableViewCell class]]) {
        aSuperview = [aSuperview superview];
    }
    YMContactCell *cell = (id) aSuperview;
    NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
    return [[self fetchedResultsController] objectAtIndexPath:indexPath];
}

It feels clunky to me. Kinda "meh"…

like image 57
Yang Meyer Avatar answered Oct 22 '22 13:10

Yang Meyer


The way I most often see it done is by assigning tags to the buttons that are equal to the indexPath.row.

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

    CustomCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
    cell.theLabel.text = self.theData[indexPath.row];
    cell.button.tag = indexPath.row;
    [cell.button addTarget:self action:@selector(doSomething:) forControlEvents:UIControlEventTouchUpInside];
    return cell;
}



-(void)doSomething:(UIButton *) sender {
    NSLog(@"%@",self.theData[sender.tag]);
    //sender.tag will be equal to indexPath.row
}
like image 22
rdelmar Avatar answered Oct 22 '22 15:10

rdelmar


Another solution:

For me something like this works flawlessly, and looks very elegant:

- (void)buttonClicked:(id)sender
      CGPoint buttonPosition = [sender convertPoint:CGPointZero
                                       toView:self.tableView];
      NSIndexPath *clickedIP = [self.tableView indexPathForRowAtPoint:buttonPosition];

      // When necessary 
      // UITableViewCell *clickedCell = [self.tableView cellForRowAtIndexPath:clickedIP];
}

Update 12/01/2017

After some time, and implementing lots of UITableViews, I need to admit that the best solution is using the delegation pattern, already suggested by others here.

like image 38
MP23 Avatar answered Oct 22 '22 15:10

MP23


Reading these answers, i would like say my opinion:

Cell by button position

- (void)buttonClicked:(id)sender
  CGPoint buttonPosition = [sender convertPoint:CGPointZero
                                   toView:self.tableView];
  NSIndexPath *clickedIP = [self.tableView indexPathForRowAtPoint:buttonPosition];

  // When necessary 
  // UITableViewCell *clickedCell = [self.tableView cellForRowAtIndexPath:clickedIP];
}

the solution above is certainly the most rapid to implement, but it is not the best from the point of view of the design/architecture. Moreover you obtain the indexPath but need to calculate any other info. This is a cool method, but would say not the best.

Cell by while cycle on the button superviews

// ContactListViewController.m

- (IBAction)emailContact:(id)sender {
    YMContact *contact = [self contactFromContactButton:sender];
    // present composer with `contact`...
}

- (YMContact *)contactFromContactButton:(UIView *)contactButton {
    UIView *aSuperview = [contactButton superview];
    while (![aSuperview isKindOfClass:[UITableViewCell class]]) {
        aSuperview = [aSuperview superview];
    }
    YMContactCell *cell = (id) aSuperview;
    NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
    return [[self fetchedResultsController] objectAtIndexPath:indexPath];
}

Get the cell in this way is more expensive of the previous and it is not elegant as well.

Cell by button tag

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

    CustomCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
    cell.theLabel.text = self.theData[indexPath.row];
    cell.button.tag = indexPath.row;
    [cell.button addTarget:self action:@selector(doSomething:) forControlEvents:UIControlEventTouchUpInside];
    return cell;
}

-(void)doSomething:(UIButton *) sender {
    NSLog(@"%@",self.theData[sender.tag]);
    //sender.tag will be equal to indexPath.row
}

Absolutely no. Use the tag can seems a cool solution, but the tag of a control can be used for a lot of things, like the next responder etc. I don't like and this is not the right way.

Cell by design pattern

// YMContactCell.h
@protocol YMContactCellDelegate
- (void)contactCellEmailWasTapped:(YMContactCell*)cell;
@end

@interface YMContactCell
@property (weak, nonatomic) id<YMContactCellDelegate> delegate;
@end

// YMContactCell.m
- (IBAction)emailContact:(id)sender {
    [self.delegate contactCellEmailWasTapped:self];
}

// ContactListViewController.m
- (void)contactCellEmailWasTapped:(YMContactCell*)cell;
    NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
    YMContact *contact = [[self fetchedResultsController] objectAtIndexPath:indexPath];
    // present composer with `contact` ...
}

This is the solution. Use delegation or use blocks is really a nice thing to do because you can pass all parameters that you want and make the architecture scalable. In fact in the delegate method (but also with blocks) you could want send directly informations without having the need to calculate them later, like the previous solutions.

Enjoy ;)

like image 21
Matteo Gobbi Avatar answered Oct 22 '22 14:10

Matteo Gobbi


Swift Closure Approach

I guess I found a new approach which is a bit swifty. Tell me what you think about it.

Your Cell:

class ButtonCell: UITableViewCell {
    var buttonAction: ( () -> Void)?
    
    func buttonPressed() {
       self.buttonAction?()
   }
}

Your UITableViewDataSource:

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "CallCell", for: indexPath)
         //Handled inside 
         cell.buttonAction = {
            //Button pressed 
         }
         //Handle in method
         cell.buttonAction = self.handleInnerCellButtonPress() 
 }

You can also pass data inside this call. Like the cell or something stored inside the cell.

Regards,
Alex

like image 29
Sn0wfreeze Avatar answered Oct 22 '22 15:10

Sn0wfreeze