How to expand section in tableview in iOS? - ios

I have a custom header for each section in tableView, and the headerView has a button. On click of that button, I'm trying to expand the section by changing the number of rows for that section. It works fine if I call reloadData, but it crashes when I try to use reloadSections/ insert/delete sections.
HEre is my number of rows method:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (highlighHeaderClicked) {
return [_expandCountArray[section]integerValue]; // 9,9,9
}
return [_collapseCountArray[section]integerValue]; //2,2,2
}
So by default tableView shows 2 rows, when button is clicked, I want to show 9 rows.
and the button action method:
-(IBAction)highlightHeaderClicked:(id)sender{
highlighHeaderClicked = !highlighHeaderClicked;
NSIndexPath *indexPath = [self getIndexPathForView:sender];
NSIndexSet *indexSet = [NSIndexSet indexSetWithIndex:indexPath.length-1];
[self.tableView beginUpdates];
[self.tableView deleteSections:indexSet withRowAnimation:UITableViewRowAnimationBottom];
[self.tableView insertSections:indexSet withRowAnimation:UITableViewRowAnimationBottom];
[self.tableView endUpdates];
}
By doing this I got the excpetion:
Invalid update: invalid number of rows in section 2. The number of rows contained in an existing section after the update (9) must be equal to the number of rows contained in that section before the update (2), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).
I also tried removing the object from datasource when the button action method gets called.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [_tempRowCountArray[section]integerValue]; //2,2,2
}
-(IBAction)highlightHeaderClicked:(id)sender{
highlighHeaderClicked = !highlighHeaderClicked;
NSIndexPath *indexPath = [self getIndexPathForView:sender];
NSIndexSet *indexSet = [NSIndexSet indexSetWithIndex:indexPath.length-1];
NSInteger rowCount = _mainDataSet.dashboardArray.count;
if (rowCount > 2) {
if (highlighHeaderClicked) {
[_tempRowCountArray replaceObjectAtIndex:indexPath.length withObject:#(rowCount)];
}else{
[_tempRowCountArray replaceObjectAtIndex:indexPath.length withObject:#2];
}
[self.tableView beginUpdates];
[self.tableView deleteSections:indexSet withRowAnimation:UITableViewRowAnimationBottom];
[self.tableView insertSections:indexSet withRowAnimation:UITableViewRowAnimationBottom];
[self.tableView endUpdates];
}
}
What am I missing here ? I think I am passing the right array count each time.

The basic problem you have is that you are trying to insert and remove whole sections. That means the number of sections would have to change.
Instead, you have to remove and insert rows in sections.
Also, you have to specify the exact difference between the previous state and the new state.
For example:
NSArray *rowsToBeAdded = ...
NSArray *rowsToBeRemoved = ...
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:rowsToBeAdded withRowAnimation: UITableViewRowAnimationBottom];
[self.tableView deleteRowsAtIndexPaths:rowsToBeRemoved withRowAnimation: UITableViewRowAnimationBottom];
[self.table endUpdates];
That also means you have to be very careful about your logic and keep track of the sections that are expanded.
You have to return the correct number of items for collapses and expanded sections:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
BOOL expanded = ...;
if (!expanded) {
return 0;
}
return ...
}
One other solution for this problem is to keep the rows always there and avoid inserts & deletes completely. You can return zero height for all hidden rows.
To update the height of all rows, you then simply call:
[self.tableView beginUpdates];
[self.tableView endUpdates];

Related

iOS UITableView number of rows in section using different array for same section

So I have a UITableView with one section that I want to update on a button click. Now the number of rows in this section are getting populated using different arrays. I choose which one to return based on an index in a dictionary. Something like this:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
// Return the number of rows in the section.
switch (section) {
case 0:
return 1;
case 1: {
NSMutableArray *array = [self.dict objectForKey:[NSNumber numberWithInteger:self.index]];
return [array count] + 1;
}
default:
break;
}
return 1;
}
So from this, section 1's rows depend on different arrays. So now in my table view, I have a button that inserts another row in section 1 and I update the appropriate array as well. Now when I hit this other button that updates this section to show the values of a different array in the self.dict data structure I get an error:
'Invalid update: invalid number of rows in section 1. The number of rows contained in an existing section after the update (3) must be equal to the number of rows contained in that section before the update (4), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'
So for example, if in my first array I have 3 cells and then the user adds one more so there are 4 cells, then if the user clicks the button which should update the section 1 to show the second array (which has 3 cells), the error above is thrown. I think this is because the data source changed and the number of rows decreased without any explicit deletion of cells. How do I fix this problem?
Updated with more code:
// Insert here when the very last cell of section 1 is selected
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSMutableArray *array = [self.dict objectForKey:[NSNumber numberWithInteger:self.index]];
if (indexPath.section == 1 && indexPath.row == [array count]) {
[array addObject:[NSNumber numberWithInt:8]];
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:[array count]-1 inSection:1]] withRowAnimation:UITableViewRowAnimationFade];
}
}
// Code for reloading the table view
- (IBAction)next:(id)sender {
self.index++;
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:UITableViewRowAnimationFade];
}
}
Instead of reload the sections, you should try reloadData:
- (IBAction)next:(id)sender {
self.index++;
[self.tableView reloadData]
}

NSFetchedResultsController: Multiple FRCs, Delegate Error when Updating

Objective: Using FRC, sort Section's by startDate, an NSDate attribute, but want Today's date Section to appear before Upcoming dates Section.
I followed Apple's code using a transient property sectionIdentifier. Apple's sample code. and started with this project first: OneFRC
I soon realized that this may not be possible with just one FRC (I could be wrong).
Next, I decided to take a stab at this with 3 FRCs: ThreeFRC.
TableView sections now appears in the Order that I want:
Section 0: Today
Section 1: Upcoming
Section 2: Past
However, adding data triggers FRC delegates, and I get the following error:
CoreData: error: Serious application error. An exception was caught from the
delegate of NSFetchedResultsController during a call to
-controllerDidChangeContent:. Invalid update: invalid number of rows in section 0.
The number of rows contained in an existing section after the update (4) must be
equal to the number of rows contained in that section before the update (3), plus
or minus the number of rows inserted or deleted from that section (0 inserted,
0 deleted) and plus or minus the number of rows moved into or out of that section
(0 moved in, 0 moved out). with userInfo (null)
Again, I would love to be able to accomplish my objective with 1 FRC, but I can't seem to figure out how.
I have been trying to resolve this for 4 days now! If this issue doesn't get resolved on SO, I think I may reach out to Apple for Developer support. And in the event that I do, I'll post the resolution here so others can benefit.
Projects are available on Github:
One FRC
Three FRC
EDIT
Thanks to #blazejmar, I was able get rid of the rows error. However, now I get an error when I attempt to add sections.
2014-11-03 16:39:46.852 FRC[64305:60b] CoreData: error: Serious application error.
An exception was caught from the delegate of NSFetchedResultsController during a
call to -controllerDidChangeContent:. Invalid update: invalid number of sections.
The number of sections contained in the table view after the update (2) must be
equal to the number of sections contained in the table view before the update (1),
plus or minus the number of sections inserted or deleted (0 inserted, 0 deleted).
with userInfo (null)
Steps to reproduce the error in Three FRC:
1. Launch App ->
2. Tap Generate Data ->
3. Tap View in FRC ->
4. Tap back to the RootVC ->
5. Change the system date to a month from Today ->
6. Tap View in FRC and only one section `Past` should appear. ->
7. Tap `Add Data`.
8. The error should appear in the log.
In your ThreeFRC project there are some issues:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
[self.tableView beginUpdates];
self.numberOfSectionsInTV = 0;
[self fetchData];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView reloadData];
[self.tableView endUpdates];
}
You shouldn't use fetchData inside FRC delegate. Methods are called in proper order (before, during and after update) so inside callbacks you have consistent state of context. Also it's not the best idea to use reloadData before endUpdates(it's applying all changes you provided earlier) and reloadData is erasing everything and building it from scratch. This is most likely causing the crash.
Other thing I've spotted that may be buggy is handling of updates. If you have 3 separate FRC without sections you won't get section update callback in FRC delegate. But if some objects appear in one of the FRC's then you should detect that and manually insert them.
Using just reloadData in controllerDidChangeContent would be enough, but this isn't the best solution, as you won't get any animations. The proper way would be to handle all the cases: deleting all objects from one of FRCs (and then deleting section manually from TableView), inserting first object into FRC (then you should create new section at proper indexPath).
i looked at your ThreeFRC project and noticed that in - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView you check which FRCs contain objects and that would determine the number of sections. this makes logical sense, but really confuses the FRC delegate when adding/deleting "sections" (or, when your other FRCs suddenly have objects). For example, you only have a Past section (1 section), but then the data changes such that you now also have a Today section. Since sectionPastFRC or the other FRCs didn't have any section changes, there are no calls to - (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type, and though you should have 2 sections now, there were no calls to add, delete, or move sections. you'd have to update the sections manually somehow, which may be a pain.
here's the workaround i suggest: since you will ALWAYS have at most one section for each FRC, you should just return 3 in - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView. This is so there will no longer be any problem in adding/deleting a section because they were all already there. Anyway, if, for example, the Today section has no objects, just return 0 in - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section. Just make sure that in - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section, if fetchedObjects==0, return nil, so that it also won't display the section header if that section has no objects. And in your FRC delegate didChangeObject, just always adjust the indexPath and newIndexPath before performing changes on the tableView.
note that this workaround will only work if you already know the maximum number of sections that the FRCs (except the last FRC) will need. it is NOT a solution for all implementations of multiple FRCs in a single table view. i've actually used this solution in a project where i had 2 FRCs for one tableView, but the first FRC would only always take up 1 section, while the second FRC could have any number of sections. i always just had to adjust the sections +1 for changes in the second FRC.
i've actually tried applying the changes i mentioned above into your code, and haven't been getting errors. here are the parts i changed in the UITableViewDataSource:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 3;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSInteger rows = 0;
switch (section) {
case 0:
{
rows = [[self.sectionTodayFRC fetchedObjects]count];
break;
}
case 1:
{
rows = [[self.sectionUpcomingFRC fetchedObjects]count];
break;
}
case 2:
{
rows = [[self.sectionPastFRC fetchedObjects]count];
break;
}
}
NSLog(#"Section Number: %i Number Of Rows: %i", section,rows);
return rows;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
NSString *header;
switch (section) {
case 0:
{
if ([[self.sectionTodayFRC fetchedObjects]count] >0)
{
header = #"Today";
}
break;
}
case 1:
{
if ([[self.sectionUpcomingFRC fetchedObjects]count] >0)
{
header = #"Upcoming";
}
break;
}
case 2:
{
if ([[self.sectionPastFRC fetchedObjects]count] >0)
{
header = #"Past";
}
break;
}
}
return header;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
if (cell == nil)
{
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:CellIdentifier];
}
Meeting *meeting;
switch (indexPath.section) {
case 0:
if ([[self.sectionTodayFRC fetchedObjects]count] > 0)
{
meeting = [[self.sectionTodayFRC fetchedObjects] objectAtIndex:indexPath.row];
}
break;
case 1:
if ([[self.sectionUpcomingFRC fetchedObjects]count] > 0)
{
meeting = [[self.sectionUpcomingFRC fetchedObjects] objectAtIndex:indexPath.row];
}
break;
case 2:
if ([[self.sectionPastFRC fetchedObjects]count] > 0)
{
meeting = [[self.sectionPastFRC fetchedObjects] objectAtIndex:indexPath.row];
}
break;
}
cell.textLabel.text = meeting.title;
return cell;
}
and for the NSFetchedResultsControllerDelegate:
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
NSLog(#"Inside didChangeObject:");
NSIndexPath *modifiedIndexPath;
NSIndexPath *modifiedNewIndexPath;
if (controller == self.sectionTodayFRC)
{
modifiedIndexPath = [NSIndexPath indexPathForRow:indexPath.row inSection:0];
modifiedNewIndexPath = [NSIndexPath indexPathForRow:newIndexPath.row inSection:0];
}
else if (controller == self.sectionUpcomingFRC)
{
modifiedIndexPath = [NSIndexPath indexPathForRow:indexPath.row inSection:1];
modifiedNewIndexPath = [NSIndexPath indexPathForRow:newIndexPath.row inSection:1];
}
else if (controller == self.sectionPastFRC)
{
modifiedIndexPath = [NSIndexPath indexPathForRow:indexPath.row inSection:2];
modifiedNewIndexPath = [NSIndexPath indexPathForRow:newIndexPath.row inSection:2];
}
switch(type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertRowsAtIndexPaths:#[modifiedNewIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
NSLog(#"frcChangeDelete");
[self.tableView deleteRowsAtIndexPaths:#[modifiedIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeUpdate:
NSLog(#"frcChangeUpdate");
[self.tableView reloadRowsAtIndexPaths:#[modifiedIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeMove:
NSLog(#"frcChangeDelete");
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:modifiedIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:modifiedNewIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
}
}
i hope this helps someone!

Add new items to UITableView without replacing old items

I intended to add items to a UITableView when this method was called. New items are successfully added (but there's a problem, mentioned below), but I think its odd because I thought "begin updates" to "end updates" was supposed to handle this (Inserting a new row). The initial if condition I had put in never ran so the whole area never got executed. I only realized this recently and updated the condition to what it is now. Now the if block get executed and it crashes the app.
When it is commented out like it is now... New items are added but the newNameOfItem replaces any existing cell labels.
I would like this to add x(newNumberOfItems) new items preferably into a new section each time its called. How can I achieve this?
- (void)addNew:(NSString *)newNumberOfItems :(NSString *)newNameOfItem
{
if(!self.numberOfRows){
NSLog(#"Initially no of rows = %d", self.numberOfRows);
self.numberOfRows = [self.numberOfItems intValue];
NSLog(#"Then no of rows = %d", self.numberOfRows);
}
else
{
self.numberOfRows = self.numberOfRows + [newNumberOfItems intValue];
NSLog(#"New no rows = %d", self.numberOfRows);
}
NSLog(#"run = %d", self.run);
Begin updates if statement ...
/*if(self.secondRun){
NSLog(#"run = %d it it", self.run);
[self.tableView beginUpdates];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:self.numberOfRows-[newnumberOfItems intValue] inSection:0];
[self.tableView insertRowsAtIndexPaths:#[indexPath]withRowAnimation:UITableViewRowAnimationBottom];
[self.tableView endUpdates];
}
*/
[self.tableView reloadData];
}
...
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"UITableViewCell" forIndexPath:indexPath];
cell.textLabel.text = self.nameOfItem;
return cell;
}
...
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.numberOfRows;
}
If you want to add new items to a new section every time you call -addNew…, you should create an NSMutableArray of sections where each member is an array of objects representing row data (in this case seems like you'd want NSStrings that represent the item's name. The structure would look something like:
mySections = #[ #[#"section 0 row 0 name", #"section 0 row 1 name", …], #[#"section 1 row 0 name", #"section 1 row 1 name", …], …]
Then in numberOfRowsInSection: return mySection[indexPath.section].count.
Each label has the same value because you're setting every single label for every cell that you dequeue to self.nameOfItem. It's doing exactly what you told it to do. If your intent is to set a different text for every section/row, you have to fetch that text from somewhere. If you created a mySections array as above, you could:
cell.textLabel.text = mySections[indexPath.section][indexPath.row] ;
A note about -addNew:…: simply adding new items to your mySections array will not cause the tableview to update. As you know above, [self.tableView reloadData] will do this for you. However, it will reload the entire table instead of just updating the rows that you added. To do this more efficiently (and with a nice animation), you instead use [self.tableView beginUpdates/endUpdates]. In the case above, where you're adding entire sections and not just rows, you should use insertSections:withRowAnimation:.

Hiding a UITableView section along with all the rows in it

I have 2 sections on my UITableview controller. One of my section has a switch and my requirement is when I set the switch to ON, the second section should be set to hidden along with all its rows. I am calling the following method/code to hide the section when the status of the switch is changed:
- (void)setState
{
myTableViewCell *myCell = [[myTableViewCell alloc] init];
if ([myCell.mySwitch isOn])
{
NSIndexPath *indexPath = [NSIndexPath indexPathWithIndex:1];
[self.tableView cellForRowAtIndexPath:indexPath].hidden = YES;
}
}
I am getting the following exception for this code which I understand is perfectly true.
Name = NSInternalInconsistencyException;
Reason = "Invalid index path for use with UITableView. Index paths passed to table view must contain exactly two indices specifying the section and row. Please use the category on NSIndexPath in UITableView.h if possible.";
But how can I hide the complete section along with all its rows. If I try to get the index path using NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:1]; this would just hide the 1st row in the section.
If you wanna hide a whole section when switch is on, just reloadData when you click the switch, and return 0 in numberOfRowsInSection for that section and return totalNumberOfSections - howManySwitchesIsOn in numberOfSectionsInTableView, like this:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
if(howManySwitchesIsOn) {
return totalNumberOfSections - howManySwitchesIsOn;
}
return totalNumberOfSections;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if(section should be hide) {
return 0;
}
return howManyRowsForThatSection;
}
Section deletion and insertion in a grouped UITableView are accomplished via:
- (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
- (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
Check out SectionHidingDemo, a demo app that illustrates section deletion and insertion in a grouped UITableView using those methods.

Custom UITableViewCell height yields improper animation

I currently am working with a uitableview that holds mostly standard size cells at 44pts. However, there are a couple that are larger, about 160pts.
In this instance, there are 2 rows at 44pts height, with the larger 160pts row being inserted below, at index 2 in the section.
Removal call:
- (void)removeRowInSection:(TableViewSection *)section atIndex:(NSUInteger)index {
NSUInteger sectionIndex = [self.sections indexOfObject:section];
NSIndexPath *removalPath = [NSIndexPath indexPathForRow:index inSection:sectionIndex];
[self.tableView beginUpdates];
[self.tableView deleteRowsAtIndexPaths:#[removalPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView endUpdates];
}
Delegate call:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
TableViewSection *section = [self sectionAtIndex:indexPath.section];
return [section heightForRowAtIndex:indexPath.row];
}
section call:
- (NSInteger)heightForRowAtIndex:(NSInteger)index {
StandardCell *cell = (StandardCell *)[self.list objectAtIndex:index];
return cell.height;
}
cell call:
- (CGFloat)height {
return 160;
}
What has me confused is when I remove the larger rows from the table, they start to animate, moving underneath the row above. But when they get to a certain point, about a 1/4 of the way through the animation, they disappear instead of finishing the animation.
It seems like the table animates the row with the notion that it's only 44pts, then once it's reached the point where 44pts are underneath the row above, it gets removed from the table. What detail have I overlooked that will give the table the correct notion to automatically animate the row removal?
Thanks for your help.
Update:
I tried commenting out the height function above (which overrides the default that returns 44). This results in a proper animation with no skips. FWIW
One way to solve this is to animate the row height down to 44 just before deleting:
//mark index paths being deleted and trigger `contentSize` update
self.indexPathsBeingDeleted = [NSMutableArray arrayWithArray:#[indexPath]];
[tableView beginUpdates];
[tableView endUpdates];
//delete row
[self.tableView deleteRowsAtIndexPaths:#[removalPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView endUpdates];
And then in your heightForRowAtIndexPath:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if ([self.indexPathsBeingDeleted containsObject:indexPath]) {
//return normal height if cell is being deleted
[self.indexPathsBeingDeleted removeObject:indexPath];
return 44;
}
if (<test for tall row>) {
return 160;
}
return 44;
}
There's a little bit of bookkeeping going on to keep track of index paths being deleted. There are probably cleaner ways to do this. This is just the first thing that came to mind. Here's a working sample project.

Resources