I am following a tutorial,where I can expand the table view by adding some sub-cells and collapse the table by removing the sub-cells. I am trying to change how the expand operation should execute. When I tap on a row,it expand and shows the sub-cells,and when I tap on other row,the previous expanded row should close. I am not able to do this . I tried the following but I couldnt make the one row expandable at a time,and the other row should close when one expands.
Note: This code works fine,but to collapse the row,we need to tap twice on the same row. I am trying to collapse when other parent is tapped.
Here is the code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
Node *node = [self nodeForIndexPath:indexPath];
// differenet table view cell prototypes for different node levels
UITableViewCell *cell;
if(node.level == 0)
{
cell = [tableView dequeueReusableCellWithIdentifier:#"level1cell" forIndexPath:indexPath];
}
else
{
cell = [tableView dequeueReusableCellWithIdentifier:#"level2cell" forIndexPath:indexPath];
}
// set the nodes title as row text
cell.textLabel.text = node.title;
// attach the nodeId for later lookup when selected
cell.tag = node.nodeId;
// Configure the cell...
return cell;
}
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
Node *node = [self nodeForIndexPath:indexPath];
// NSLog(#"node id is %ld level is %ld and indexpath.row is %d",(long)node.nodeId,(long)node.level,indexPath.row);
node.expanded = !node.expanded;
if (node.level==0) {
NSLog(#"you tapped parent");
//now check other parents are expanded or not
}else{
NSLog(#"you tapped child");
}
//insertion always happen after deletion
// NSLog(#"you touched at %#,index row is %d",indexPath,indexPath.row);
if(node.expanded )
{
// add n rows
NSMutableArray *indexPaths = [NSMutableArray array];
for(NSInteger i=indexPath.row; i< indexPath.row+node.subNodes.count; i++)
{
[indexPaths addObject:[NSIndexPath indexPathForRow:i+1 inSection:0]];
}
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];
}
else
{
//you store the node refe after row is expanded
// delete n rows
NSMutableArray *indexPaths = [NSMutableArray array];
for(NSInteger i=indexPath.row; i< indexPath.row+node.subNodes.count; i++)
{
[indexPaths addObject:[NSIndexPath indexPathForRow:i+1 inSection:0]];
}
[self.tableView beginUpdates];
[self.tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];
}
}
#pragma mark - Private helper
-(Node*) nodeForIndexPath:(NSIndexPath*)indexPath
{
int idx = 0;
for(Node *node in _nodes)
{
if(idx == indexPath.row)
{
return node;
}
idx++;
if(node.expanded)
{
for (Node *subNode in node.subNodes)
{
if(idx == indexPath.row)
{
return subNode;
}
idx++;
}
}
}
return nil;
}
KMAccordionTableViewController can helps you?
https://github.com/klevison/KMAccordionTableViewController
Hm... If your current code is working, but you need to tap twice to collapse the open/selected row, perhaps it's because didDeselectRowAtIndexPath: is being called during that first tap in place of didSelectRowAtIndexPath: in order to deselect the selected row. I'd recommend configuring your tableview to allow for multiple selection so that didSelectRowAtIndexPath: is called every time, ex:
- (void)viewDidLoad {
[super viewDidLoad];
tableView.allowsMultipleSelection = YES;
}
Related
I have a UITableView having many rows that contains UITextFields. User enters data to these fields. user can delete or add any rows. When i am trying to delete any row it deletes the correct row from the array(that contains all the cell's reference) but the UITextfield always shows that the last row is deleted.
-(NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return _countEmailValues.count ;
}
-(NSInteger) numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
-(UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIdentifier = #"CustomEmailCell";
cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];
if (!cell) {
cell = [[LACustomEmailCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
}
cell.emailTextField.tag = 555;
cell.deleteEmailFieldButton.tag = indexPath.row;
NSLog(#"delete email Field tag %i",cell.deleteEmailFieldButton.tag );
cell.emailTextField.delegate = self;
if ([_countEmailValues count] > 1 )
{
cell.deleteEmailFieldButton.hidden = false;
}
else
{
cell.deleteEmailFieldButton.hidden = true;
}
// Reason why I am adding cell.emailtextfield in this delegate? is should be in addButtonclick but cell.emailtextfield is not
// initialized there. Also adding of only cell will give null for the emailTextField.
// So adding all cells here and then removing duplicates entires and accessing over the app.
[_emailValues addObject:cell.emailTextField];
// Remove Duplicate elements.
NSArray *emailFieldCollection = [_emailValues copy];
NSInteger index = [emailFieldCollection count ] -1;
for (id object in [emailFieldCollection reverseObjectEnumerator])
{
if ([_emailValues indexOfObject:object inRange:NSMakeRange(0, index)] != NSNotFound)
{
[_emailValues removeObjectAtIndex:index];
}
index--;
}
NSLog(#"Row : %i",indexPath.row);
return cell;
}
- (IBAction)deleteEmailClick:(UIButton *)sender
{
NSIndexPath *index = self.emailTableView.indexPathForSelectedRow;
// [self.emailTableView selectRowAtIndexPath:[NSIndexPath indexPathForRow:indexPath.row inSection:0] animated:YES scrollPosition:0];
NSIndexPath *indexPath = [_emailTableView indexPathForSelectedRow];
NSLog(#"Tags %i", sender.tag);
// [self.emailTableView reloadData];
if ([_countEmailValues count] > 0)
{
[ _countEmailValues removeObjectAtIndex:sender.tag];
}
// NSLog(#"array after %#",_countEmailValues);
if ([_countEmailValues count] == 0)
{
// _deleteEmailButton.hidden = true;
_doneButton.hidden = true;
}
NSLog(#"array before %#",_emailValues);
if ([_emailValues count] > 0)
{
[_emailValues removeObjectAtIndex:sender.tag];
}
[self.emailTableView reloadData];
}
The _emailValues gets updated properly but the data in the fields is always getting cleared of the last one.
EX: in the image if i delete "b" the _emailValues gets cleared properly but the ui shows two field having data "a" & "b" . what have i missed here?
pls help.
You are not deleting the cell . In deleteEmailClick add this line
[self.emailTableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationLeft];
Also I would like to point out another way of implementing delete using a block. This will ensure that you always have the correct cell and no need to tag your delete button. This would be cleaner and maintainable. In your custom cell class LACustomEmailCell declare a block property like so
#property (strong, nonatomic) void (^deleteButtonTappedBlock)();
Connect the IBAction of your delete button in the LACustomEmailCell class and from there call this block. I am assuming the name as deleteButtonPressed
- (IBAction)deleteButtonPressed:(id)sender {
self.deleteButtonTappedBlock();
}
Now in -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
You can set the block like this
[cell setDeleteButtonTappedBlock:^(){
//Action to perform when cell is deleted. In your case the code in `deleteEmailClick` will go here.
//In the end delete the cell.
[self.emailTableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}];
i hope this helps
I have a tableView with some sections, which all have a footer, and then I have a tableViewFooter on the Tableview itself.
If I scroll down to the bottom of my tableview and delete the last item(therefore deleting the section altogether) in any sections above the last section (second last and up) it gives me this error
2014-02-21 13:19:55.066 xxxx[5436:60b] *** Assertion failure in -[UIViewAnimation initWithView:indexPath:endRect:endAlpha:startFraction:endFraction:curve:animateFromCurrentPosition:shouldDeleteAfterAnimation:editing:], /SourceCache/UIKit/UIKit-2903.23/UITableViewSupport.m:2661
Uncaught exception: Cell animation stop fraction must be greater than start fraction
at endUpdates
this is my code
[self.tableView beginUpdates];
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
if(indexPath != nil){
TableSection * sec = [self.sections objectAtIndex:indexPath.section];
NSMutableDictionary *dic =[sec.items objectAtIndex:indexPath.row];
Product* product = [dic valueForKey:PRODUCT];
//removing the item in the section
[sec.items removeObject:dic];
//deleting item from products
NSMutableArray *temp = [NSMutableArray array];
for (Product *p in self.dataCon.listPersister.products) {
if ([p.product.objId isEqualToString: product.product.objId]) {
[temp addObject:p];
}
}
for (Product *p in temp) {
[self.dataCon.listPersister.products removeObject:p];
}
//if it was the last object in section, delete the section else just delete the single row
if(sec.items.count == 0)
{
[self.sections removeObject:sec];
[self.footers removeObjectAtIndex:indexPath.section];
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:indexPath.section] withRowAnimation:UITableViewRowAnimationFade];
} else
{
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
USFooterView *footer = [self.footers objectAtIndex:indexPath.section];
footer.totalLabel.text = [self.dataCon.listPersister getTotalForShopId:sec.title];
self.footerView.totalLabel.text = [self.dataCon.listPersister getTotalForAllProducts];
}
}
[self.tableView endUpdates];
I had the same code earlier, just without my tableView and table sections having footers, where it worked, so I think they might be the problem, but I'm not entirely sure that's the reason it's acting up.
I have seen this post
UITableView tableFooterView may cause crash?
And the post that it links to, but that didn't help me.
Any help is appreciated :)
In the else statement you delete row from table view:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
But not from data source. Delete row from array which you use as data source and it should works.
I found a "fix", but I'm avoiding the use of sectionFooter, because that seems to be bugged.
I created an extra cell at the end of each section, with the same setup I had for my footer View before, and made that last cell not editable with
-(BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
TableSection * sec = [self.sections objectAtIndex:indexPath.section];
if (sec.items.count != indexPath.row) {
return YES;
} else
return NO;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [sec.items count] +1 ;
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"normalcell";
static NSString *CellIdentifier1 = #"footercell";
TableSection * sec = [self.sections objectAtIndex:indexPath.section];
if (indexPath.row != sec.items.count) {
//use normal type of cell
return cell;
} else{
//use footer type of cell
return cell;
}
}
So the last cell Imitates a "footer", but it's not stuck to the bottom of the frame, but I'll have to live with that. It's better than crashes.
Try using UITableViewRowAnimationLeft or UITableViewRowAnimationRight as the delete row animation(deleteRowsAtIndexPaths:withRowAnimation:).
It crashed for me when using UITableViewRowAnimationAutomatic, but not with the other two. I have not tried all of them but it seems to be a bug with the animation code for some of the options.
I am trying to achive a email like select all functionality in uitableview where on same button tap user can checkmark or remove all checkmark and additionally user can also select/deselect rows(on didSelectRowAtIndexPath). I tried to do but its not working properly, here is my code.
- (IBAction)selectAll:(id)sender
{
if(myBoolean)
{
for (NSInteger s = 0; s < self.iTable.numberOfSections; s++)
{
for (NSInteger r = 0; r < [self.iTable numberOfRowsInSection:s]; r++)
{
[[self.iTable cellForRowAtIndexPath:[NSIndexPath indexPathForRow:r inSection:s]] setAccessoryType:UITableViewCellAccessoryNone];
}
}
myBoolean = NO;
[_selectUnselectButton setTitle:#"Select all Friends" forState:UIControlStateNormal];
}
else
{
for (NSInteger s = 0; s < self.iTable.numberOfSections; s++)
{
for (NSInteger r = 0; r < [self.iTable numberOfRowsInSection:s]; r++)
{
[[self.iTable cellForRowAtIndexPath:[NSIndexPath indexPathForRow:r inSection:s]] setAccessoryType:UITableViewCellAccessoryCheckmark];
NSLog(#"%d-%d",s,r);
}
}
myBoolean = YES;
[_selectUnselectButton setTitle:#"Unselect all Friends" forState:UIControlStateNormal];
}
}
-(void)tableView:(UITableView *)tableView_ didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView_ cellForRowAtIndexPath:indexPath];
[cell setSelectionStyle:UITableViewCellSelectionStyleNone];
if (cell.accessoryType == UITableViewCellAccessoryCheckmark)
{
cell.accessoryType = UITableViewCellAccessoryNone;
}
else
{
cell.accessoryType = UITableViewCellAccessoryCheckmark;
}
}
I wrote a sample code that I adapted to your needs.
Basically it is
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *unifiedID = #"aCellID";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:unifiedID];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:unifiedID];
}
cell.textLabel.text = [self.states objectAtIndex:indexPath.row];
//if the indexPath was found among the selected ones, set the checkmark on the cell
cell.accessoryType = ([self isRowSelectedOnTableView:tableView atIndexPath:indexPath]) ? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone;
return cell;
}
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *state = [self.states objectAtIndex:indexPath.row];
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
if([self isRowSelectedOnTableView:tableView atIndexPath:indexPath]){
[self.selectedCells removeObject:indexPath];
[self.selecedStates removeObject:state];
cell.accessoryType = UITableViewCellAccessoryNone;
} else {
[self.selectedCells addObject:indexPath];
[self.selecedStates addObject:state];
cell.accessoryType = UITableViewCellAccessoryCheckmark;
}
NSLog(#"%#", self.selecedStates);
}
-(BOOL)isRowSelectedOnTableView:(UITableView *)tableView atIndexPath:(NSIndexPath *)indexPath
{
return ([self.selectedCells containsObject:indexPath]) ? YES : NO;
}
- (IBAction)selectAll:(id)sender {
[self.selecedStates removeAllObjects];
[self.selectedCells removeAllObjects];
NSUInteger numberOfSections = [self.tableView numberOfSections];
for (NSUInteger s = 0; s < numberOfSections; ++s) {
NSUInteger numberOfRowsInSection = [self.tableView numberOfRowsInSection:s];
for (NSUInteger r = 0; r < numberOfRowsInSection; ++r) {
NSIndexPath *idxPath = [NSIndexPath indexPathForRow:r inSection:s];
[self.selectedCells addObject:idxPath];
[self.selecedStates addObject:self.states[idxPath.row]];
}
}
[self.tableView reloadData];
}
- (IBAction)deselectAll:(id)sender {
[self.selecedStates removeAllObjects];
[self.selectedCells removeAllObjects];
[self.tableView reloadData];
}
- (IBAction)toggleAll:(id)sender {
if ([self.states count] == [self.selecedStates count]) {
[sender setTitle:#"select all"];
[self deselectAll:sender];
} else {
[sender setTitle:#"deselect all"];
[self selectAll:sender];
}
}
in action:
You are calling
[[self.iTable cellForRowAtIndexPath:[NSIndexPath indexPathForRow:r inSection:s]] setAccessoryType:UITableViewCellAccessoryNone];
for every row in every section within the tableView. if you have many row, this is ver inefficient, as it will deal with rows not on the screen. But this is not needed. just put every selected index path into an array and tell the tableView to reload. This will reload the visible cells and due to the implementation of -tableView:cellForRowAtIndexPath: cells wfor new rows will be correctly re-conigured.
Setting the accessory view needs to happen inside the tableView:cellForRowAtIndexPath: method. When you want to change the accessories from outside, the outside method needs to change the model first to indicate that check marks must be placed in certain cells, and then call reloadData on the UITableView.
One way to store what cells are checked is an array of NSIndexSet objects - one index set per section. In the example below I show code for a single section, but you should get an idea of how to make multiple sections work.
// This variable needs to be declared in a place where your data source can get it
NSMutableIndexSet *selected;
// You need to initialize it in the designated initializer, like this:
selected = [[NSMutableIndexSet alloc] init];
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
}
if ([selected containsIndex:indexPath.row]) {
[cell setAccessoryType:UITableViewCellAccessoryCheckmark];
} else {
[cell setAccessoryType:UITableViewCellAccessoryNone];
}
// Do the rest of your code
return cell;
}
Now in the code where you want to set rows selected or unselected you just need to call [selected addIndex:rowToSelect] or [selected removeIndex:rowToUnselect], and call your table's reloadData.
Use selectRowAtIndexPath:animated:scrollPosition: to select a row
and deselectRowAtIndexPath:animated: to deselect a row.
For more read the UITableView docs
Try this code instead of your old one
- (IBAction)selectAll:(id)sender
{
if(myBoolean)
{
for (NSInteger s = 0; s < self.iTable.numberOfSections; s++)
{
for (NSInteger r = 0; r < [self.iTable numberOfRowsInSection:s]; r++)
{
[self.iTable selectRowAtIndexPath:[NSIndexPath indexPathForRow:r inSection:s]] animated:YES scrollPosition:UITableViewScrollPositionNone];
}
}
myBoolean = NO;
[_selectUnselectButton setTitle:#"Select all Friends" forState:UIControlStateNormal];
}
else
{
for (NSInteger s = 0; s < self.iTable.numberOfSections; s++)
{
for (NSInteger r = 0; r < [self.iTable numberOfRowsInSection:s]; r++)
{
[self.iTable selectRowAtIndexPath:[NSIndexPath indexPathForRow:r inSection:s]] animated:YES scrollPosition:UITableViewScrollPositionNone];
NSLog(#"%d-%d",s,r);
}
}
myBoolean = YES;
[_selectUnselectButton setTitle:#"Unselect all Friends" forState:UIControlStateNormal];
}
}
-(void)tableView:(UITableView *)tableView_ didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView_ cellForRowAtIndexPath:indexPath];
[cell setSelectionStyle:UITableViewCellSelectionStyleNone];
if (cell.accessoryType == UITableViewCellAccessoryCheckmark)
{
cell.accessoryType = UITableViewCellAccessoryNone;
}
else
{
cell.accessoryType = UITableViewCellAccessoryCheckmark;
}
}
You can follow these steps to implement this,
1) You should have a mutable array to store indexpaths
2) What you can do is, when you tap Check All or Uncheck All, do add or remove all indexpaths to/from array (which you've), reload table for update changes
3) In - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath datasource method, check into array using if([array containsObject:indexPath]), if exist mark it checked or unchecked
4) In - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath delegate method, you should check for already existence of indexpath tapped into array, as you've did in 3rd step, and add or remove indexpaths as per the condition, reload table for update changes
Take another NSMutableArray as SelectedArray
in didSelectRowAtIndexPath row You can Add remove objects from SelectedArray.
You can select a UITableViewCell calling UITableViews selectRowAtIndexPath method:
[yourtable selectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]
animated:NO
scrollPosition:UITableViewScrollPositionTop];
Put for loop for selectedArray to putcheckmark only in selected cells or All cells.
check my accepted answer here : Need to create a select all button for UITableView and add selections to an array in iOS
Xcode 8x
Swift 3x
Select Row
let indexPath = IndexPath(row: 0, section: 0)
mytableView.selectRow(at: indexPath, animated: true, scrollPosition: .bottom)
myTableView.delegate?.tableView!(myTableView, didSelectRowAt: indexPath)
DeSelect Row
let deselectIndexPath = IndexPath(row: 7, section: 0)
myTableView.deselectRow(at: deselectIndexPath, animated: true)
myTableView.delegate?.tableView!(tblView, didDeselectRowAt: indexPath)
you can do something like this:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[self doSomethingWithRowAtIndexPath:indexPath];
}
I'm trying to setup my table so that the user can select one item per section.
For example:
- Section 0
- Row 0 √
- Row 1
- Section 1
- Row 0
- Row 1 √
- Row 2
So in the above example if the user selects section 0, row 1 then row 0 in the same section should be unchecked and the selected row gets a checkmark.
Same goes for section 1 where any selected row should get a checkmark and then I want to remove the checkmark from the previously selected row in the same section.
- Section 0
- Row 0
- Row 1 √
- Section 1
- Row 0
- Row 1
- Row 2 √
Please keep in mind that I won't have a predefined number of sections or rows, so the code should work for this scenario. Here's what I currently have, hopefully that might get me started on the right path.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
MyObject *myObject = [self.fetchedResultsController objectAtIndexPath:indexPath];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
if ([myObject.optionSelected boolValue] == NO) {
[myObject setOptionSelected:[NSNumber numberWithBool:YES]];
[cell setAccessoryType:UITableViewCellAccessoryCheckmark];
} else {
[myObject setOptionSelected:[NSNumber numberWithBool:NO]];
[cell setAccessoryType:UITableViewCellAccessoryNone];
}
if ([tableView numberOfRowsInSection:indexPath.section] > 1) {
NSMutableArray *cells = [[NSMutableArray alloc] init];
for (NSInteger i = 0; i < [tableView numberOfRowsInSection:indexPath.section]; ++i) {
[cells addObject:[tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:i inSection:indexPath.section]]];
}
for (UITableViewCell *deselectCell in cells) {
if ([self.tableView indexPathForCell:deselectCell] != indexPath && deselectCell != cell) {
MyObject *tempObject = [self.fetchedResultsController objectAtIndexPath:[self.tableView indexPathForCell:deselectCell]];
[tempObject setOptionSelected:[NSNumber numberWithBool:NO]];
[cell setAccessoryType:UITableViewCellAccessoryNone];
}
}
}
}
As you can see I am also setting the object's selected state, I'd like this to remain intact :)
Thanks for any feedback and help in advance!
Your tableview should declare a mutable array to hold the currently selected paths:
NSMutableArray *selectedCellPaths = [[NSMutableArray alloc] init];
Then your tableView:didSelectRowAtIndexPath: should look like this
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
MyObject *myObject = [self.fetchedResultsController objectAtIndexPath:indexPath];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
if ([myObject.optionSelected boolValue] == NO) {
[myObject setOptionSelected:[NSNumber numberWithBool:YES]];
[cell setAccessoryType:UITableViewCellAccessoryCheckmark];
[selectedCellPaths addObject:indexPath];
} else {
[myObject setOptionSelected:[NSNumber numberWithBool:NO]];
[cell setAccessoryType:UITableViewCellAccessoryNone];
if ([selectedCellPaths containsObject:indexPath]) {
[selectedCellPaths removeObject:indexPath];
}
}
// Now we're going to remove all but the cell path that is actually selected.
NSMutableArray *cellsToRemove = [[NSMutableArray alloc] init];
for (NSIndexPath *selectedCellIndexPath in selectedCellPaths) {
if ([selectedCellIndexPath compare:indexPath] != NSOrderedSame && selectedCellIndexPath.section == indexPath.section) {
// deselect cell at selectedCellPath
[cellsToRemove addObject:selectedCellIndexPath];
}
}
[selectedCellPaths removeObjectsInArray:cellsToRemove];
}
Note I have just put in a comment where you would want to deselect the actual cell at the cell path that is not selected. You need to fill that code in yourself.
I haven't tested this code, just modified what you had in TextMate but it should work barring minor changes.
I have an app with a table view that expands/collapses sections, following the example in Apple's Table View Animations & Gestures sample app. I am running into problems when an item is added to a closed section: after that, the section no longer opens, and I get an exception when I try to open and then close it.
I've traced this to some strange behaviour in the open/close methods:
-(void)sectionHeaderView:(SectionHeaderView*)sectionHeaderView sectionOpened:(NSInteger)section {
if (![[sectionHeaderArray objectAtIndex:section] isOpen]) {
[[sectionHeaderArray objectAtIndex:section] setIsOpen:YES];
NSLog(#"self.tableView: %#", self.tableView);
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
NSInteger countOfRowsToInsert = [sectionInfo numberOfObjects];
NSMutableArray *indexPathsToInsert = [[NSMutableArray alloc] init];
for (NSInteger i = 0; i < countOfRowsToInsert; i++) {
[indexPathsToInsert addObject:[NSIndexPath indexPathForRow:i inSection:section]];
}
// Apply the updates.
[self.tableView beginUpdates];
NSLog(#"Count of rows to insert: %d", [indexPathsToInsert count]);
NSLog(#"Rows before insert: %d", [self.tableView numberOfRowsInSection:section]);
[self.tableView insertRowsAtIndexPaths:indexPathsToInsert withRowAnimation:UITableViewRowAnimationTop];
NSLog(#"Rows after insert: %d", [self.tableView numberOfRowsInSection:section]);
[self.tableView endUpdates];
}
}
-(void)sectionHeaderView:(SectionHeaderView*)sectionHeaderView sectionClosed:(NSInteger)section {
if ([[sectionHeaderArray objectAtIndex:section] isOpen]) {
[[sectionHeaderArray objectAtIndex:section] setIsOpen:NO];
NSInteger countOfRowsToDelete = [self.tableView numberOfRowsInSection:section];
if (countOfRowsToDelete > 0) {
NSMutableArray *indexPathsToDelete = [[NSMutableArray alloc] init];
for (NSInteger i = 0; i < countOfRowsToDelete; i++) {
[indexPathsToDelete addObject:[NSIndexPath indexPathForRow:i inSection:section]];
}
[self.tableView beginUpdates];
NSLog(#"Count of rows to delete: %d", [indexPathsToDelete count]);
NSLog(#"Rows before delete: %d", [self.tableView numberOfRowsInSection:section]);
[self.tableView deleteRowsAtIndexPaths:indexPathsToDelete withRowAnimation:UITableViewRowAnimationTop];
NSLog(#"Rows after delete: %d", [self.tableView numberOfRowsInSection:section]);
}
[self.tableView endUpdates];
}
}
The log messages show that, on open (insert rows), >0 rows are being inserted, and yet the row count for that section stays 0:
2012-03-31 13:36:17.454 QuickList7[5523:fb03] Count of rows to insert: 3
2012-03-31 13:36:17.454 QuickList7[5523:fb03] Rows before insert: 0
2012-03-31 13:36:17.454 QuickList7[5523:fb03] Rows after insert: 0
This sets up an inconsistent state between the table and data source, and then when I try to "collapse" the section, I get the following exception:
2012-03-31 13:48:35.783 QuickList7[5523:fb03] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid table view update. The application has requested an update to the table view that is inconsistent with the state provided by the data source.'
How can I insert 3 rows, and still end up with 0 rows?
Thanks,
Sasha
I found the problem! It was actually in the fetchedResultsController's change handler. It was responding to changes to closed sections, which left the table in a bad state, and out of sync with the data source. So I added a check for each update to only insert/delete/update rows if the containing section is open.
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tv = self.tView;
switch(type) {
case NSFetchedResultsChangeInsert:
if ([[sectionHeaderArray objectAtIndex:newIndexPath.section] isOpen]) {
[tv insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
}
break;
case NSFetchedResultsChangeDelete:
if ([[sectionHeaderArray objectAtIndex:indexPath.section] isOpen]) {
[tv deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
break;
case NSFetchedResultsChangeUpdate:
if ([[sectionHeaderArray objectAtIndex:indexPath.section] isOpen]) {
[self configureCell:[tv cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
}
break;
case NSFetchedResultsChangeMove:
if ([[sectionHeaderArray objectAtIndex:indexPath.section] isOpen]) {
[tv deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
if ([[sectionHeaderArray objectAtIndex:newIndexPath.section] isOpen]) {
[tv insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]withRowAnimation:UITableViewRowAnimationFade];
}
break;
}
}
In my app I've implemented a similar behavior in a very different way because I was running into this type of problem a lot.
I have a table with MenuNameCells, MenuItemCells and a static cell at the bottom. Only one menu is expanded at a time, and tapping a MenuNameCell expands or collapses that menu. Since I keep the MenuNameCell in its own section and the MenuItemCells in another, I only have to insert/delete entire sections when I reload the table.
Here's my table's data source:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// number of menus, plus 1 if a menu is open, plus 1 static cell
return [self.restaurant.menus count]+(self.menu != nil ? 1 : 0)+1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// if this section is our selected menu, return number of items, otherwise return 1
int numberOfRowsInSection = ([self indexPathIsInMenuItemSection:section] ? [[self.menu items] count] : 1);
return numberOfRowsInSection;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.section == [tableView numberOfSections]-1) {
// ... set up and return static cell
}
if ([self indexPathIsInMenuItemSection:indexPath.section]) {
// ... set up and return menu item cell
} else {
// ... set up and return menu name cell
}
}
and my table's delegate:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[tableView deselectRowAtIndexPath:indexPath animated:YES];
// return if it's a static cell
if (indexPath.section==[tableView numberOfSections]-1)
return;
// if it's a menu name cell, close open menu and maybe expand this menu
if (![self indexPathIsInMenuItemSection:indexPath.section]) {
BOOL reset = self.menu == m;
if (reset) [self reloadTableView:self.tableView withMenu:nil animated:YES autoscroll:NO];
else [self reloadTableView:self.tableView withMenu:m animated:YES autoscroll:YES];
}
}
There were a couple of helpers mentioned in there:
- (BOOL)indexPathIsInMenuItemSection:(NSInteger)section
{
// returns YES if section refers to our MenuItemCells
int indexOfMenu = [self.restaurant getIndexOfMenu:self.menu];
return indexOfMenu != -1 && section == indexOfMenu+1;
}
- (void)reloadTableView:(UITableView *)tableView withMenu:(Menu *)menu animated:(BOOL)animated autoscroll:(BOOL)autoscroll
{
int oldIndex = [self.restaurant getIndexOfMenu:self.menu];
int newIndex = [self.restaurant getIndexOfMenu:menu];
[tableView beginUpdates];
if (oldIndex != -1) {
// index of [section for items] is oldIndex+1
[tableView deleteSections:[NSIndexSet indexSetWithIndex:oldIndex+1] withRowAnimation:UITableViewRowAnimationTop];
}
if (newIndex != -1) {
// index for [section for items] is newIndex+1
[tableView insertSections:[NSIndexSet indexSetWithIndex:newIndex+1] withRowAnimation:UITableViewRowAnimationTop];
[self setMenu:menu];
} else {
// no new menu
[self setMenu:nil];
}
[tableView endUpdates];
if (autoscroll) [self autoscroll];
}
- (void)autoscroll
{
if (self.menu != nil) {
int section = [self.restaurant getIndexOfMenu:self.menu];
if (section != -1) {
NSUInteger indexes[] = {section,0};
NSIndexPath *indexPath = [NSIndexPath indexPathWithIndexes:indexes length:2];
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:YES];
}
}
}
Since my data is loaded asynchronously elsewhere, I have this controller set up to receive an NSNotification, but it should work just as well to call this on viewDidAppear:
[self reloadTableView:self.tableView withMenu:self.menu animated:YES autoscroll:YES];
I hope this helps! Let me know if I can clarify any of it.