Best practice: handling actions from buttons inside tableview cells? - ios

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

Related

How can I create a 30 row UITableView, without doing it statically, and accept UITextField input on each row?

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.

Custom UITableView Cell with UIButton not Calling didSelectRowAtIndexPath

I understand this is suppose to happen but I haven't been able to find a way to call this method when the button is tapped The method gets called but the wrong cell is selected.
- (UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath object:(PFObject *)object{
_postCell = (postCell *) [self.tableView dequeueReusableCellWithIdentifier:#"postCell"];
_postCell.personStringPost.text = [object objectForKey:#"stringPost"];
[_postCell.nameLabel setTitle:[object objectForKey:#"User_Name"] forState:UIControlStateNormal];
_postCell.userId = [object objectForKey:#"userId"];
_postCell.selectionStyle = UITableViewCellSelectionStyleNone;
PFFile *imageFile = [object objectForKey:#"profileImage"];
NSData *data = [imageFile getData];
_postCell.profileImage.image = [UIImage imageWithData:data];
[_postCell.nameLabel addTarget:self action:#selector(personProfile:tableView:) forControlEvents: UIControlEventTouchUpInside];
[_postCell.profileImageButton addTarget:self action:#selector(personProfile:tableView:) forControlEvents:UIControlEventTouchUpInside];
return _postCell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[super tableView:tableView didSelectRowAtIndexPath:indexPath];
postCell *cell = (postCell *)[tableView cellForRowAtIndexPath:indexPath];
self.userId = cell.userId;
NSLog(#"Did Select: %#", self.userId);
}
- (void) personProfile: (NSIndexPath *) indexPath tableView: (UITableView *) tableView{
[self tableView:tableView didSelectRowAtIndexPath:indexPath];
[self performSegueWithIdentifier:#"personProfile" sender:self];
}
Few things i hope it will help
while dealing with button with only one section
in cellfor row method
yourcell.mybutton.tag=indexPath.row;
-(void)myCellbuttonAction:(id)sender
{
UIButton *button = (UIButton *)sender; // first, cast the sender to UIButton
NSInteger row = button.tag; // recover the row
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0];
// apply your logic for indexpath as in didselect row
}
While dealing with multiple sections and multiple rows it this might help you
in cell for row method
yourcell.tag=indexPath.section;
yourcell.contentview.tag=indexPath.row;
and your button action might look like this
-(void)myCellbuttonAction:(id)sender
{
UIButton *button = (UIButton *)sender; // first, cast the sender to UIButton
id rowid =[button superview];
id sectionid = [rwoid superview];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[rowid tag] inSection:[sectionid tag]];
// apply your logic for indexpath as in didselect row
}
A few things here.
a) I would not use an instance variable (_postCell) in your cellForRowAtIndex method. You will likely have cell re-use problems and this may well be the source of your error. Replace it with a local variable:
postcell *cell = (postCell *)[self.tableView dequeueReusableCellWithIdentifier:#"postCell"];
You will also need to replace all references to _postCell with cell.
b) Note that in the same line your cast uses lowercase = (postCell *)... - I have done the same above, but it is best practice for class names to start with capital letters.
c) You have a property named nameLabel, which suggests its a UILabel, but you are using the setTitle:forState: method, which implies it's a UIButton. I would rename this property since debugging will be a lot easier if the names match the classes (or at least don't imply the wrong class).
d) When you call the addTarget:action:forControlEvents method, your selector is for personProfile:tableView:. The signature for that method is for an NSIndexPath and a UITableView. But your button will not be sending those arguments of those types. It will send details of the sender - i.e. the button which triggered the action. So you need to revise your method to accept arguments of that type:
[cell.nameLabel addTarget:self action:#selector(buttonPressed:) forControlEvents: UIControlEventTouchUpInside];
e) When the method gets called, you need a way to determine which cell the sending button was on. Ideally you would subclass UIButton to add some link to the cell, but (if you have only one section) you might get away with putting the row number as a tag. To do this add:
cell.nameLabel.tag = indexPath.row;
to your cellForRowAtIndexPath:. Then you can implement a different method to handle the button press, as follows:
-(void)buttonPressed:(id)sender {
UIButton *button = (UIButton *)sender; // first, cast the sender to UIButton
NSInteger row = button.tag; // recover the row
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0]; // derive the indexPath, assuming section is 0
[self.tableView selectRowAtIndexPath:indexPath animated:YES]; // select the relevant row in the table (assuming the table is self.tableView)
[self performSegueWithIdentifier:#"personProfile" sender:self]; // perform the segue
}
f) Note in the above that you should not call tableView:didSelectRowAtIndexPath:. This is for the tableView to call when the user selects the row, not for selecting rows programmatically. Use selectRowAtIndexPath:animated: instead.
This is basically pbasdf answer above with Swift 3
I do call didSelectRowAt directly since it is not triggered by selectRowAtIndexPath
The tag is set on cell creation with the row of the indexpath
The getTableView function is my own
#IBAction func actionButtonPushed(_ sender: Any) {
guard let button = sender as? UIButton else { return }
guard let tableView = getTableView() else { return }
let indexPath = IndexPath(row: button.tag, section: 0)
tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
if let delegate = tableView.delegate {
delegate.tableView!(tableView, didSelectRowAt: indexPath)
}
}

Radio button logic in UItableViewCells

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!

custom UITableViewCell know what cell was a check box modified

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

iOS - Loop through Cells and retrieve data

Sorry I'm pretty new to iOS dev.
I have a UITableView setup from cells being pulled from a single XiB nib. I've created a on/off switch in the nib, and I am trying to save the state of the switch upon viewWillDisappear for the number of cells that I have. (6 cells to be exact).
How can I loop through all the cells and save this information?
I tried this in my UIViewController to get the info for one cell:
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
UITableView *tv = (UITableView *)self.view;
UITableViewCell *tvc = [tv cellForRowAtIndexPath:0];
}
it gives me the error "Program received signal: "EXC_BAD_INSTRUCTION".
How can I accomplish this?
You have to pass a valid NSIndexPath to cellForRowAtIndexPath:. You used 0, which means no indexPath.
You should use something like this:
UITableViewCell *tvc = [tv cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
BUT. Don't do this. Don't save state in the UITableViewCell.
Update your dataSource when a switch changed its state.
If you have implemented the UITableViewDataSource methods the right why your tableView reuses cells. That means the state of your cells will vanish when the cells are reused.
Your approach might work for 6 cells. But it will fail for 9 cells.
It will probably even fail if you scroll the first cell off screen.
I wrote a quick demo (if you don't use ARC add release where they are necessary) to show you how you should do it instead:
- (void)viewDidLoad
{
[super viewDidLoad];
self.dataSource = [NSMutableArray arrayWithCapacity:6];
for (NSInteger i = 0; i < 6; i++) {
[self.dataSource addObject:[NSNumber numberWithBool:YES]];
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
UISwitch *aSwitch = [[UISwitch alloc] init];
[aSwitch addTarget:self action:#selector(switchChanged:) forControlEvents:UIControlEventValueChanged];
cell.accessoryView = aSwitch;
}
UISwitch *aSwitch = (UISwitch *)cell.accessoryView;
aSwitch.on = [[self.dataSource objectAtIndex:indexPath.row] boolValue];
/* configure cell */
return cell;
}
- (IBAction)switchChanged:(UISwitch *)sender
{
// UITableViewCell *cell = (UITableViewCell *)[sender superview];
// NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
CGPoint senderOriginInTableView = [sender convertPoint:CGPointZero toView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:senderOriginInTableView];
[self.dataSource replaceObjectAtIndex:indexPath.row withObject:[NSNumber numberWithBool:sender.on]];
}
as you see it's not very complicated to not store state in the cells :-)
Moving [super viewDidDisappear:animated]; to the end of your method may be the most expedient way to address the problem. If that does not work, move the logic into viewWillDisappear:animated:.
A better way to deal with this would be to avoid reading the current state from the view at all. Rather, the view should pass the state to the model on each update. This way you would be able to harvest the current state from your model, entirely independently from the state of your view.

Resources