I have a custom cell that have a check box,
all is working fine, the checkboxes get check according to a dictionary that I pass to my subclassed UITableViewCell,
but now I need to pass to the class that have the table view the exact cell that my check box was modified so I can set my mutable dictionary with the new checked or unchecked state for that particular cell,
So how to do this?, shall I use a delegate?, this is fine, but the question is, how do I know what cell was my check box modified at?
You can use a delegate like this...
MyCell.h
#protocol MyCellDelegate <NSObject>
-(void)cellCheckBoxWasChanged:(MyCell *)cell;
#end
#interface MyCell : NSObject
#property (nonatomic, weak) id <MyCellDelegate> delegate;
#end
MyCell.m
#implementation MyCell
- (void)checkBoxChanged
{
[self.delegate cellCheckBoxWasChanged:self];
}
#end
Then to find the index you can do...
TableViewController.m
- (void)cellCheckBoxWasChanged:(MyCell *)cell
{
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
// do something to your array.
}
Why don't you pass the UITableViewCell also in the delegate method as self.
So with that cell, you could get the indexpath by
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
set the tag Value of each Checkboxes depending on the cells Indexpath in the cellForRowAtIndexPath: method.
UIButton *checkboxes = customCell.checkButton
[checkboxes setTag:indexPath.row];
then in buttons action method.
check the senders.Tag value to get the exact row of the button pressed
UITableViewCell *cell = [tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:sender.tag inSection:0]];
you can do this -
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:nil];
if (cell == nil)
{
cell=[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
}
if([[[Usercontacts objectAtIndex:indexPath.row]objectForKey:#"isChecked"]isEqualToString:#"NO"])
{
[checkUncheckBtn setImage:[UIImage imageNamed:#"unchecked box.png" ]forState:UIControlStateNormal];
checkUncheckBtn.tag=1;
}
else
{
[checkUncheckBtn setImage:[UIImage imageNamed:#"checked box.png" ]forState:UIControlStateNormal];
checkUncheckBtn.tag=2;
}
[checkUncheckBtn addTarget:self action:#selector(checkUncheckBtnPressed:) forControlEvents:UIControlEventTouchUpInside];
return cell;
}
and when you perform checkUncheckBtnPressed: method it looks like
-(void)checkUncheckBtnPressed:(id)sender
{
UIButton *btn=(UIButton*)sender;
UITableViewCell *cell =(UITableViewCell *) [sender superview] ;
NSIndexPath *_indxpath = [createGroupContactsTableView indexPathForCell:cell];
if(btn.tag==1)
{
[btn setImage:[UIImage imageNamed:#"checked box.png" ]forState:UIControlStateNormal];
btn.tag=2;
[[Usercontacts objectAtIndex:_indxpath.row]setObject:#"YES" forKey:#"isChecked"];
}
else
{
[btn setImage:[UIImage imageNamed:#"unchecked box.png" ]forState:UIControlStateNormal];
btn.tag=1;
[[Usercontacts objectAtIndex:_indxpath.row]setObject:#"NO" forKey:#"isChecked"];
}
}
Here's an alternative to having the Cells listen to events from the checkBoxes and forward them to the UITableViewController using the delegate pattern:
Have the UITableViewController listen to events from the checkBoxes and use the following code to determine the NSIndexPath of the cell:
#implementation UITableView (MyCategory)
-(NSIndexPath*)indexPathOfCellComponent:(UIView*)component {
if([component isDescendantOfView:self] && component != self) {
CGPoint point = [component.superview convertPoint:component.center toView:self];
return [self indexPathForRowAtPoint:point];
}
else {
return nil;
}
}
#end
Related
I'm trying to get the indexPath.row of a button clicked inside a tableView row.
When the user clicks this button I get the index.row corresponding to the button very well, but when I add more objects to the source array to create more cells by calling reloadData, the rowButtonClicked in each cell it's no longer giving me the correct indexPath.row in example I press the index 20 and now the printed indexPath.row is 9.
In cellForRowAtIndexPath to add the event to the button:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
lBtnWithAction = [[UIButton alloc] initWithFrame:CGRectMake(liLight1Xcord + 23, 10, liLight1Width + 5, liLight1Height + 25)];
lBtnWithAction.tag = ROW_BUTTON_ACTION;
lBtnWithAction.titleLabel.font = luiFontCheckmark;
lBtnWithAction.tintColor = [UIColor blackColor];
lBtnWithAction.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
[cell.contentView addSubview:lBtnWithAction];
}
else
{
lBtnWithAction = (UIButton *)[cell.contentView viewWithTag:ROW_BUTTON_ACTION];
}
//Set the tag
lBtnWithAction.tag = indexPath.row;
//Add the click event to the button inside a row
[lBtnWithAction addTarget:self action:#selector(rowButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
return cell;
}
To do something with the clicked index:
-(void)rowButtonClicked:(UIButton*)sender
{
//Get the index of the clicked button
NSLog(#"%li", (long)sender.tag);
[self doSomething:(long)sender.tag];
}
Constants.h
#define ROW_BUTTON_ACTION 9
Why it is giving an incorrect index when I change the initial items of the tableView? Is there a way to solve this issue?
It looks like you're messing up button tags. Once you set the tag
lBtnWithAction.tag = indexPath.row;
you won't be able to get button correctly with
lBtnWithAction = (UIButton *)[cell.contentView viewWithTag:ROW_BUTTON_ACTION];
(assuming ROW_BUTTON_ACTION is a constant). lBtnWithAction will be nil all the time except when indexPath.row is equal to ROW_BUTTON_ACTION.
I would propose to subclass UITableViewCell, add a button-property there and then just refer to it directly instead of searching by tag. In this case you'll be able to use tags for buttons freely :) –
#interface UIMyTableViewCell : UITableViewCell
#property (nonatomic, strong, nonnull) UIButton *lBtnWithAction;
#end
And then in cellForRowAtIndexPath:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UIMyTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UIMyTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
[cell.lBtnWithAction addTarget:self action:#selector(rowButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
}
cell.lBtnWithAction.tag = indexPath.row;
return cell;
}
You can simply update you line -
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
to
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath]
My take on this will be subclassing the UITableViewCell and providing a delegate for it to respond.
code:
//MyCoolTablewViewCell.h
#protocol MyCoolProtocol<NSObject>
-(void)youTouchedMyCoolCell:(MyCoolTableViewCell __nonnull *)myCoolCell;
#end
#interface MyCoolTableViewCell:UITableViewCell
#property(nonatomic, weak,nullable) id<MyCoolProtocol>myCooldelegate;
#end
//MyCoolTablewViewCell.m
#interface MyCoolTableViewCell(){
UIButton *mySuperCoolButton;
}
#end
#implementation MyCoolTablewViewCell
-(id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier{
if(!(self = [super initWithStyle:style reuseIdentifier:reuseIdentifier])){
return nil;
}
// init your stuff here
// init button
mySuperCoolButton = [UIBUtton buttonWithType:UIButtonTypeCustom];
[mySuperCoolButton addTarget:self action:#selector(tappedMyCoolButton:) forControlEvents:UIControlEventTouchUpInside];
return self;
}
- (void)tappedMyCoolButton:(id)sender{
if([_myCoolDelegate respondToSelector:#selector(youTouchedMyCoolCell:)]){
[_myCoolDelegate youTouchedMyCoolCell:self];
}
}
#end
then in your controller
// whatEverController.m
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// get the cell
cell = [tableView dequeueReusableCellWithIdentifier:#"myCoolIdentifier"forIndexPath:indexPath];
cell.myCooldelegate = self;
}
-(void)youTouchedMyCoolCell:(MyCoolTableViewCell *)myCoolCell{
NSIndexPath *myCoolIndexPath = [_tableView indexPathForCell:myCoolCell];
// then do whatever you want with the index path
}
i have looked up the same questions, but the answers given don't seem to fix my problem.
I'm creating an app where you can add and remove your school marks. In my view controller i have created a UITableView. Within there there's a custom UITableViewCell, called TableViewCell. To delete a cell i need to get it's row. I try that by using the following code in the TableViewCell class:
- (IBAction)DeleteMark:(id)sender {
UITableView *superTableView = [self superview];
NSIndexPath *path = [superTableView indexPathForCell:self];
NSInteger *index = [path row];
NSLog([NSString stringWithFormat:#"%#", index]);
}
When i run the code it shows the following error:
-[UITableViewWrapperView indexPathForCell:]: unrecognized selector sent to instance 0xa825a00
As i said before i tried the solutions given on the forums, but none of them worked.
EDIT
Here's my code for cellForRowAtIndexPath
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
TableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"cell" forIndexPath:indexPath];
float weight = [[_editableClass.markWeights objectAtIndex:indexPath.row]floatValue];
float mark = [[_editableClass.classMarks objectAtIndex:indexPath.row]floatValue];
cell.MarkTextLabel.text = [NSString stringWithFormat:#"%.1f", mark];
cell.MarkWeight.text = [NSString stringWithFormat:#"%.1f", weight];
[cell.DeleteButton setTag:indexPath.row];
return cell;
}
To do something like what you are after you could try to set up a delegate to pass back the cell object so you can find the index from the tableView. I have tried to make an example below. For tidiness I have omitted import statements.
ViewController header file
#interface subClassViewController : UIViewController<UITableViewDataSource, UITableViewDelegate,TableViewCellDelegate>
#property (weak, nonatomic) IBOutlet UITableView *tableView;
#end
ViewController implementation file
#implementation subClassViewController
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath
{
//setup cell...
cell.delegate = self;
return cell;
}
- (void)deletePressedOnCell:(UITableViewCell *)cell
{
NSInteger index = [self.tableView indexPathForCell:cell].row;
// do something with index
}
#end
TableViewCell header file
#protocol TableViewCellDelegate
- (void)deletePressedOnCell:(UITableViewCell *)cell;
#end
#interface TableViewCell : UITableViewCell
#property (weak, nonatomic) id<TableViewCellDelegate>delegate;
#end
TableViewCell implementation file
#implementation TableViewCell
- (IBAction)DeleteMark:(id)sender
{
[self.delegate deletePressedOnCell:self];
}
#end
In your UITableview Delegate method cellForRowAtIndexPath set tag of your button:-
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
.....
cell.yourdeletebutton.tag=indexPath.row;
}
Now in your delete method it looks like:-
- (IBAction)DeleteMark:(id)sender {
UIButton *tappedButton = (UIButton*)sender;
DeleteIndex=tappedButton.tag;
NSLog(#"delete row at indexpath :%#", DeleteIndex]);
}
You can use by [sender tag],
[cell.btnDelete setTag:indexPath.row];
[cell.btnDelete addTarget:self action:#selector(DeleteMark:) forControlEvents:UIControlEventTouchUpInside];
Method,
- (IBAction) DeleteMark:(id)sender {
NSInteger *index = [sender tag];
NSLog([NSString stringWithFormat:#"%#", index]);
// Your Code...
//For Eg.
Object *obj = [YOUR_ARRAY objectAtIndex:[sender tag]];
// You'll get object of particular index.
// Your Code...
}
Thanks.
You shouldn't rely on [[[[sender superview] superview] superview] superview], what if Apple decide to make some changes and add/remove another layer of subview.
The idea with tags is not ideal neither.
I would recommended you to use delegate pattern or blocks.
Please have a look how closures works. Note that it's written in swift but it could be easily translated to Objective-C.
In your TableViewCell add handler property:
var buttonPressHandler: (() -> ())?
Add IBAction which must be connected to button touch event (you could do it in storyboard):
#IBAction func buttonPressed(sender: AnyObject) {
// Call handler when button is pressed
buttonPressHandler?()
}
In cellForRowAtIndexPath you only needs to set up the handler:
cell.buttonPressHandler = {
print("Button pressed: \(indexPath.row)")
}
Get the index path of the row using following code
- (void)tapped:(UIButton *) sender {
UITableViewCell * tCell;
NSComparisonResult order = [[UIDevice currentDevice].systemVersion compare: #"7.2" options: NSNumericSearch];
if (order == NSOrderedDescending) {
tCell = (BusinessFavoriteCell *)[[[[sender superview] superview] superview] superview];
} else {
tCell = (BusinessFavoriteCell *)[[[sender superview] superview] superview];
}
NSIndexPath * iPath = [self.tableView indexPathForCell:tCell];
}
Here, tCell is the UITableViewCell which is loaded in cellForRowIndexpath method.
Normally, if I wanted to have a UITextField as a part of a UITableViewCell, I would likely use either a) static rows or b) I would create the cell in the storyboard, outlet the cell and outlet the field to my ViewController, and then drag the cell outside of the "Table View", but keeping it in the scene.
However, I need to create a View where I accept input from 28 various things. I don't want to outlet up 28 different UITextField's.
I want to do this dynamically, to make it easier. So I've created a custom UITableViewCell with a Label and UITextField.
My ViewController has two arrays.
#property (nonatomic, strong) NSArray *items;
#property (nonatomic, strong) NSArray *itemValues;
My cellForRowAtIndexPath looks something like this...
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *cellIdentifier = #"ItemCell";
MyItemTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];
if (!cell) {
cell = [[MyItemTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
[cell.itemValue addTarget:self action:#selector(textFieldDidChange:) forControlEvents:UIControlEventEditingChanged];
} else {
if (![cell.itemValue actionsForTarget:self forControlEvent:UIControlEventEditingChanged]) {
[cell.itemValue addTarget:self action:#selector(textFieldDidChange:) forControlEvents:UIControlEventEditingChanged];
}
}
cell.item.text = [self.items objectAtIndex:indexPath.row];
cell.itemValue.text = [self.itemValues objectAtIndex:indexPath.row];
return cell;
}
- (void)textFieldDidChange:(id)sender
{
NSLog(#"textFieldDidChange: %zd", [self.tableView indexPathForSelectedRow].row);
}
This is proving to be problematic. textFieldDidChange always returns [self.tableView indexPathForSelectedRow].row as 0, as the cell of course, has never been selected. I'm stumped on how I could even find out which row's UITextField has been edited, so I can update the corresponding itemValues array.
UITableView has a neat method that converts a point in the tableView to an indexPath, indexPathForRowAtPoint:.
First you have to convert the origin of your textField to the frame of the UITableView.
- (void)textFieldDidChange:(UITextField *)sender
{
CGPoint textFieldOriginInTableView = [sender convertPoint:CGPointZero toView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:textFieldOriginInTableView];
if (indexPath) {
NSLog(#"TextField at indexPath %# did change", indexPath);
}
else {
NSLog(#"Error: Can't calculate indexPath");
}
}
The easiest way to do this is to just tag the textfield with indexPath.row and get it back via [sender tag] in the delegate method.
Hey I'm working on a screen where user have option groups for example "Drink" which is title of section in my tableView, and their choices are "7up", "coke", etc which are cells of my table.
Now every Option Group choice (every cell in order words) has one radio button. I want to implement this. I'm facing problem if user selects any cell's radio button then other radio buttons should be deselected but how?
any help please
You should create a function to check your radio button from your custom cell and implements a delegate method to inform your TableViewController that your button on that cell was selected.
Your TableViewController needs to implements that delegate (dont forget to set each cell.delegate = self).
Then in your delegate method you create a loop to uncheck all of the radio buttons of the cells in the section except the cell you just checked.
Something like that :
This is a custom UITableViewCell with a button.
The images checked and uncheck need to look like a radio button checked and uncheked
Here is the .h file :
//RadioCell.h
#protocol RadioCellDelegate <NSObject>
-(void) myRadioCellDelegateDidCheckRadioButton:(RadioCell*)checkedCell;
#end
#interface RadioCell : UITableViewCell
-(void) unCheckRadio;
#property (nonatomic, weak) id <RadioCellDelegate> delegate;
#end
This is the .m file of RadioCell
//RadioCell.m
#property (nonatomic, assign) UIButton myRadio;
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString*)reuseIdentifier
_myRadio = [UIButton buttonWithType:UIButtonTypeCustom];
[_myRadio setImage:[UIImage imageNamed:#"uncheck"] forState:UIControlStateNormal];
[_myRadio setImage:[UIImage imageNamed:#"check"] UIControlStateSelected];
[_myRadio addTarget:self action:#selector(radioTouched)orControlEvents:UIControlEventTouchUpInside];
_myRadio.isSelected = NO;
//don't forget to set _myRadio frame
[self addSubview:_myRadio];
}
-(void) checkRadio {
_myradio.isSelected = YES;
}
-(void) unCheckRadio {
_myradio.isSelected = NO;
}
-(void) radioTouched {
if(_myradio.isSelected == YES) {
return;
}
else {
[self checkRadio]
[_delegate myRadioCellDelegateDidCheckRadioButton:self];
}
}
Now just adapt your tableview controller with RadioCell (in .m file)
//MyTableViewController.m
#interface MyTableViewController () <RadioCellDelegate>
#end
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"RadioCell";
RadioCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[RadioCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
cell.textLabel = #"Coke"; //or whatever you want
cell.delegate = self;
return cell;
}
-(void) myRadioCellDelegateDidCheckRadioButton:(RadioCell*)checkedCell {
NSIndexPath *checkPath = [self.tableView indexPathForCell:checkedCell];
for (int section = 0; section < [self.tableView numberOfSections]; section++) {
if(section == checkPath.section) {
for (int row = 0; row < [self.tableView numberOfRowsInSection:section]; row++) {
NSIndexPath* cellPath = [NSIndexPath indexPathForRow:row inSection:section];
RadioCell* cell = (CustomCell*)[tableView cellForRowAtIndexPath:cellPath];
if(checkPath.row != cellPath.row) {
[cell unCheckRadio];
}
}
}
}
}
Simple solution for a 2-option radio button UITableView (but you get the idea):
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[tableView deselectRowAtIndexPath:indexPath animated:YES];
NSIndexPath *newIP;
if (!indexPath.row)
newIP = [NSIndexPath indexPathForRow:indexPath.row+1 inSection:0];
else
newIP = [NSIndexPath indexPathForRow:indexPath.row-1 inSection:0];
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
if (cell.accessoryType == UITableViewCellAccessoryCheckmark)
cell.accessoryType = UITableViewCellAccessoryNone;
else{
cell.accessoryType = UITableViewCellAccessoryCheckmark;
UITableViewCell *newCell = [tableView cellForRowAtIndexPath:newIP];
newCell.accessoryType = UITableViewCellAccessoryNone;
}
}
One solution would be to make use of the table views native selection capabilities.
In a standard UITableView it's only possible to have one row selected at a time and you can use this to your advantage. By setting "Selection" in storyboard to "None" the selection of a row will not be visible.
Now you can implement your own selection display. You can override the method -(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath to update your cell when it gets selected.
And you can override the method -(void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath to change the cell when it's no longer selected.
UITableViewDelegate automatically calls didSelectRowAtIndexPath on the old selection, when a new selection is made, keeping the selection unique like radio buttons.
I put together a little sample project for you to try, you can download it here.
Hopefully, I have been at least a bit helpful.
Cheers!
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!
Approach 2:
Make the cells handle the action and call a custom delegate method.
// 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` ...
}
Doesn’t handling events in a view violate the MVC principle?
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:
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 ;)
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
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"…
I would provide another approach, just doesn't assign the active target, the event will traverse upon the responder chain:
[self.actionButton addTarget:nil action:#selector(onActionButtonClick:) forControlEvents:UIControlEventTouchUpInside];
and then, in your view controller:
- (void)onActorButtonClick:(id)sender {
if ([sender isKindOfClass:UIButton.class]) {
UITableViewCell *cell = [self findAncestorTableCell:(UIView *)sender]; //See other answer to fetch the cell instance.
NSIndexPath *indexPath = [self.listTable indexPathForCell:cell];
...
}
}
However, this begets some compiler warning, add this to ignore them:
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
...
#pragma clang diagnostic pop