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!
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.
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.
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"…
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
}
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.
Reading these answers, i would like say my opinion:
- (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.
// 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.
- (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.
// 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 ;)
I guess I found a new approach which is a bit swifty. Tell me what you think about it.
class ButtonCell: UITableViewCell {
var buttonAction: ( () -> Void)?
func buttonPressed() {
self.buttonAction?()
}
}
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With