I am trying to show a table view with 2 sections. The first section will always have 1 row and the second section will have as many rows as data points. I am using Core Data and the following tableView:numberOfRowsInSection: method...
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if (section == 0) {
return 1;
} else {
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
}
}
However, I am getting the following error:
Terminating app due to uncaught exception 'NSRangeException', reason:
'* -[__NSArrayM objectAtIndex:]: index 1 beyond bounds [0 .. 0]'
Any help will be appreciated. Thanks.
NEW -------------------------------------------------------------------------------
This is the current implementation of the relevant methods:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if (section == 0) {
return 1;
} else {
NSUInteger frcSection = section - 1;
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:frcSection];
return [sectionInfo numberOfObjects];
}
}
- (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];
}
[self configureCell:cell atIndexPath:indexPath];
return cell;
}
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.section == 0) {
cell.textLabel.text = entityOne.name; //entityOne object passed from previous VC
} else {
entityTwo = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = entityTwo.name;
}
}
- (void)controller:(NSFetchedResultsController *)controller
didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex
forChangeType:(NSFetchedResultsChangeType)type
{
NSUInteger frcSectionIndex = 0;
frcSectionIndex = sectionIndex + 1;
switch(type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:frcSectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:frcSectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller
didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
The reason is that the fetched results controller (FRC) has only one section (section #0), which you want to be displayed in the second section (section #1) of the table view.
This is possible, but you have to map between FRC section numbers and table view section numbers, e.g.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if (section == 0) {
return 1;
} else {
NSUInteger frcSection = section - 1;
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:frcSection];
return [sectionInfo numberOfObjects];
}
}
The same mapping is necessary in cellForRowAtIndexPath.
In the FRC delegate methods didChangeObject, didChangeSection you have to add 1 to the section number before calling the table view methods (e.g. insertRowsAtIndexPaths).
ADDED: configureCell should look like this:
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.section == 0) {
cell.textLabel.text = entityOne.name; //entityOne object passed from previous VC
} else {
NSIndexPath *frcIndexPath = [NSIndexPath indexPathForRow:indexPath.row inSection:(indexPath.section - 1)];
entityTwo = [self.fetchedResultsController objectAtIndexPath:frcIndexPath];
cell.textLabel.text = entityTwo.name;
}
}
and didChangeObject like this:
- (void)controller:(NSFetchedResultsController *)controller
didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.tableView;
NSIndexPath *tvIndexPath = [NSIndexPath indexPathForRow:indexPath.row inSection:(indexPath.section + 1)];
NSIndexPath *tvNewIndexPath = [NSIndexPath indexPathForRow:newIndexPath.row inSection:(newIndexPath.section + 1)];
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:#[tvNewIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:#[tvIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:tvIndexPath] atIndexPath:tvIndexPath];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:#[tvIndexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:#[tvNewIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
You probably get the idea:
When going from FRC index path to table view index path, add one to the section.
When going from table view index path to FRC index path, subtract one from the section.
Related
When deleting an object from my Core Data that is fetched using NSFetchedResultsController and displayed in a TableView, it does not update the table. It deletes the object just fine however the row remains there until I swap views and return. I have noticed that this issue has only started happening since iOS8 but could be wrong. Below is my code:
#pragma mark - Fetched Results Controller Delegate
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}
// Override to support editing the table view.
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
NSManagedObjectContext * context = [self managedObjectContext];
entity * rowToDelete = [self.fetchedResultsController objectAtIndexPath:indexPath];
[context deleteObject:rowToDelete];
//check that the row has deleted data
NSLog(#"Shhiiiiiiiiii...... You done did delete a row...");
NSError * error = nil;
if (![context save:&error]){
NSLog(#"Error: %#", error);
}
//causes a crash
//[self.tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationRight];
//DOES NOT UPDATE THE TABLE
[self.tableView reloadData];
}
}
I use all the normall delegates like so:
- (void) controllerWillChangeContent:(NSFetchedResultsController *)controller{
[self.tableView beginUpdates];
}
- (void) controllerDidChangeContent:(NSFetchedResultsController *)controller{
[self.tableView endUpdates];
}
- (void) controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath{
switch (type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationRight];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationRight];
break;
case NSFetchedResultsChangeUpdate:{
entity * details = [self.fetchedResultsController objectAtIndexPath:indexPath];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
cell.textLabel.text = details.detailString;
}
break;
case NSFetchedResultsChangeMove:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
-(void) controller:(NSFetchedResultsController *)controller didChangeSection:(id<NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type{
switch (type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:[NSIndexSet indexSetWithIndex: sectionIndex] withRowAnimation:UITableViewRowAnimationRight];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationRight];
break;
case NSFetchedResultsChangeMove:
NSLog(#"A table item was moved");
break;
case NSFetchedResultsChangeUpdate:
NSLog(#"A table item was updated");
break;
}
}
I have searched on Stack and find the generic response "You need to use [self.tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationRight];". This does not work.
Thanks in advance for any help. If I do not respond immediately it's because I am taking a breather or asleep ;-)
NSFetchedResultsController should be able to communicate all the changes for you.
Check that the update functions are being called (i.e. narrow down where the failure might be)
Check that the NSFetchedResultsController instance has the delegate set
Check that you are working the the same, or connected, context (i.e. check that there is even a chance of the notification propagating)
I'm not sure if you can ignore re-ordering (might depend on your approach) but imagine that [object A, row 1] is swapped with [object B, row 2], then object B is deleted, how does the system know which table row to delete (unless you do something extra with the information)
Manually deleting a row from the table will cause a crash as the data source will be out of line with the table -- thus, causing all manner of confusion. The delegate methods for the results are there to enable the synchronisation of the actual results with those shown in the table.
I cut the following out of a working demo (although modified a bunch of stuff on the way). It works and receives changes, and updates the table. There are a few blog posts only a google away that will help too.
- (void)viewDidLoad {
[super viewDidLoad];
_fetchedResultsController = /* my fetched results controller... */;
_fetchedResultsController.delegate = self;
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
NSInteger count = [[_fetchedResultsController sections] count];
return count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
id sectionInfo = [[_fetchedResultsController sections] objectAtIndex:section];
NSInteger count = [sectionInfo numberOfObjects];
return count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
return [self configureCell:nil atIndexPath:indexPath];
}
- (UITableViewCell *)configureCell:(UITableViewCell*)cell atIndexPath:(NSIndexPath*)indexPath {
id obj = [self.fetchedResultsController objectAtIndexPath:indexPath];
if(!cell) {
cell = [self.tableView dequeueReusableCellWithIdentifier:#"MyTableViewCell" forIndexPath:indexPath];
}
cell.textLabel.text = obj.someProperty;
return cell;
}
#pragma mark NSFetechResults delegate
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
[self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id )sectionInfo
atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
switch(type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
default:
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath {
UITableView *tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath]
atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView endUpdates];
}
I stupidly placed
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView endUpdates];
}
At the beginning before the actions could update. Rookie mistake. Hope it helps anyone else who falls for the same thing.
In my iPad application, I have used a UICollectionView which displays different notes saved by the user in core data through NSFetchedResultsController.
According to the design, I must have to show a cell with control to add new note in core data. Once the note is added it is displayed as the 2nd cell in the collection view.
I tried to implement it in following manner;
#pragma mark - UICollectionViewDataSource
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView*)collectionView
{
return self.fetchedResultsController.sections.count?:1;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection: (NSInteger)section
{
id<NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects]?:1;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.section == 0 && indexPath.item == 0)
{
AddNewNoteCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"AddNewNoteCell" forIndexPath:indexPath];
cell.delegate = self;
return cell;
}
else
{
NoteCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"NoteCell" forIndexPath:indexPath];
//Code for customization
return cell;
}
}
But this approach is causing problems. It doesn't add the newly created note cell immediately. If it adds, the newly added cell is displayed as "Add new cell". If I pop the view controller and load the view controller again it displays the cells correctly.
Please guide me for an accurate solution.
Is this possible?
Assuming you want the "add new note" cell to appear as the first item in the first section, but not in any other sections, you need to modify your numberOfItemsInSection method to add 1 to the [sectionInfo numberOfObjects] for section 0. For the other sections, you should just return the numberOfObjects:
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection: (NSInteger)section
{
id<NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
NSInteger items = [sectionInfo numberOfObjects];
if (section == 0) items++;
return items;
}
Then in cellForItemAtIndexPath: you can (as you have) treat item 0 in section 0 as your special case (i.e. give it a different cell subclass and configure it accordingly). For the other cells, you can configure them using the indexPath to get details from your fetchedResultsController. However, in section 0, you need to adjust this: cell 0 is your "Add new note" cell, so you want cell 1 to be configured with the details of item 0 from your fetchedResultsController. Construct a new indexPath:
NSIndexPath *fetchIndexPath = [NSIndexPath indexPathForItem:(indexPath.row -1) inSection:0];
and use that to get the correct data from your fetchedResultsController.
I assume your "Add new note" cell calls some method in your view controller to add the new note object to your managedObjectContext. If you implement the NSFetchedResultsController delegate methods (see docs), you can immediately update your collectionView with the newly added object. But you must reverse the above adjustment for section 0:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
[self.collectionView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
switch(type) {
case NSFetchedResultsChangeInsert:
[self.collectionView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]];
break;
case NSFetchedResultsChangeDelete:
[self.collectionView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]];
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
NSIndexPath *adjustedIndexPath;
NSIndexPath *adjustedNewIndexPath;
if (indexPath.section == 0) {
adjustedIndexPath = [NSIndexPath indexPathForItem:(indexPath.row+1) inSection:0];
adjustedNewIndexPath = [NSIndexPath indexPathForItem:(newIndexPath.row+1) inSection:0];
indexPath = adjustedIndexPath;
newIndexPath = adjustedNewIndexPath;
}
switch(type) {
case NSFetchedResultsChangeInsert:
[self.collectionView insertItemsAtIndexPaths:#[newIndexPath]];
break;
case NSFetchedResultsChangeDelete:
[self.collectionView deleteItemsAtIndexPaths:#[indexPath]];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:[self.collectionView cellForItemAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[self.collectionView moveItemAtIndexPath:indexPath toIndexPath:newIndexPath];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.collectionView endUpdates];
}
You should tailor the above methods to suit your particular needs (eg. I assume you don't want to permit a section to be moved ahead of section 0).
I have a Core Data based table view that has a simple delete method. It is straightforward code that can be found in countless tutorials, however, it crashes the app with the following message:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (2) 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, 1 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'
The delete method is as follows:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete) {
// get garden to delete, delete it through Core Data
Garden *gardenToDelete = [self.fetchedResultsController objectAtIndexPath:indexPath];
NSLog(#"Garden name: %#", gardenToDelete.gardenName);
[self.managedObjectContext deleteObject:gardenToDelete];
[self.managedObjectContext save:nil];
[self.tableView beginUpdates];
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationRight];
[self.tableView endUpdates];
}
}
I have spent hours rearranging the code, removing the beginUpdate and endUpdate calls, checking other solutions. Help is greatly appreciated.
You just need to delete the object from your NSFetchedResultsController, the fetched results controller will then update the table view assuming you've implemented the delegate methods as described in the docs. You shouldn't touch the table view rows, the NSFetchedResultsControllerDelegate does all of that.
-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath{
if (editingStyle == UITableViewCellEditingStyleDelete) {
// deleting cell from fetched results contoller
Garden *gardenToDelete = [self.fetchedResultsController objectAtIndexPath:indexPath];
NSLog(#"Garden name: %#", gardenToDelete.gardenName);
[self.managedObjectContext deleteObject:gardenToDelete];
[self.managedObjectContext save:nil];
}
}
Do you implement - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section with a return value with NSFetchedResultsController? After you do save, self.fetchedResultsController will refresh, but make sure all of the dataSource of tableView return value is depended on self.fetchedResultsController.
Do you implement these methods:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
// The fetch controller is about to start sending change notifications, so prepare the table view for updates.
[self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
UITableView *tableView = self.tableView;
// my comment: mapIndexPathFromFetchResultsController function, I just use to get indexPath. You can remove it
indexPath = [self mapIndexPathFromFetchResultsController:indexPath];
newIndexPath = [self mapIndexPathFromFetchResultsController:newIndexPath];
switch(type) {
case NSFetchedResultsChangeInsert:
{
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
case NSFetchedResultsChangeDelete:
{
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
case NSFetchedResultsChangeUpdate:
{
UITableViewCell *cell;
cell = [tableView cellForRowAtIndexPath:indexPath];
if (cell != nil)
{
// my comment: just use the same with cellForRowAtIndexPath
// - (void)syncViewConfigureCell:(UITableViewCell *)cell {
// cell.textLabel.text = #""; etc. }
[self syncViewConfigureCell:cell];
}
break;
}
case NSFetchedResultsChangeMove:
{
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
switch(type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
// The fetch controller has sent all current change notifications, so tell the table view to process all updates.
[self.tableView endUpdates];
}
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.
My app has an UITableViewController as the root controller and a modal view to add a row to this table. I am using CoreData, so I get the data from a NSFetchedResultsController. Anyway, the problem is in the manage of the UITableView.
Each insertion makes TableView add a new cell. Everything works fine except if the cell is added in the first row. In this case its content is not displayed. The cell is blank displayed. If I tap in the cell or I scroll the table in a way the cell must be reloaded it does show its content.
I am using animations so I do [tableView beginUpdates] and [tableView endUpdates] as Apple docs said, instead of [tableView reloadData]. If I do [tableView reloadData] all works fine.
I have checked it and same code is runned for every row. It is a problem about the way the cells are displayed.
I think the problem is about "theory" of animations in TableViews and you won't probably need it, but there is the relevant code in my UITableViewController:
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
Customer *managedObject = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = managedObject.name;
}
#pragma mark - Table view data source
- (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];
}
[self configureCell:cell atIndexPath:indexPath];
return cell;
}
#pragma mark - FetchedResultsController delegate
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
[self.tableView beginUpdates];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView endUpdates];
// [self.tableView reloadData]; this make all works OK but without animations
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath {
UITableView *tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
In your switch statement for each type of change, why not add:
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
You will have to vary that code slightly depending on the type of change, but this would eliminate the need for beginUpdates and endUpdates.