I have a UITableView that is controlled by a NSFetchedResultsController.
Rows are organised into sections from a property of the row. But when I modify that property the application terminated with:
Invalid update: invalid number of sections.
Each row represents a shot in a film, wide shot, close up etc..
The row also contains which scene it belongs to and the sections are calculated from that.
Shot.m (CoreData entity)
#pragma mark Transient properties
- (NSString *)sectionIdentifier
{
return [self sceneNumber];
}
ShotsViewController.m (The UITableView)
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
id <NSFetchedResultsSectionInfo> theSection = [[self.fetchedResultsController sections] objectAtIndex:section];
return [theSection name];
}
I don't know what i'm supposed to do here. I've played around with the UITableView insertSections and deleteSections but I always get same invalid number of sections error.
Edit
it moves automatically to other existing sections:
- (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;
case NSFetchedResultsChangeUpdate:
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
It worked after I added the
case NSFetchedResultsChangeUpdate:
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
But now, say i've got two cells.
Cell1.sceneNumber = 1
Cell2.sceneNumber = 2
If I set Cell1.sceneNumber = 3 I get the error:
Invalid update: invalid number of sections. The number of sections contained in the table view after the update (3) must be equal to the number of sections contained in the table view before the update (2)
We'd need to see a lot more code to know what the problem is. However, one thing that might be useful is checking out the "Mastering Table Views" video from last year's WWDC. They go over the specifics of how to add/remove sections/rows and the order which you should make the change in your model/table (with clear diagrams).
Go to https://developer.apple.com/videos/wwdc/2010/ and after logging in, go to Application Frameworks > Session 128 - Mastering Table Views.
Related
My app currently uses a regular UITableView, which is populated via a NSFetchedResultsController. I also use the NSFetchedResultsControllerDelegate to update the table view. The rows are grouped by sections, which represent dates. This is also handled by the NSFetchedResultsController.
It works quite well, except when heavy changes to the sections are done in one step. I give an example here:
My tableview looks like this:
[10/10/2016]
ROW 0.0
[10/08/2016]
ROW 1.0
ROW 1.1
Now I want to change the item ROW 0.0. I change the date from 10/10/2016 to 10/09/2016.
// EDIT //
To be precise: My NSManagedObject subclass has a property NSDate* startingDate;, which is stored in the database. And when the user hits a save button I get an NSDate object from my UIDatePicker, make sure it's not nil and set the property to it. Afterwards I save the context.
// EDIT //
What happens is that the NSFetchedResultsControllerDelegate get's called three times. First with a delete section 0 and then with and insert section 0 (although those two changed order when I executed several times). And then with an update row 0.0.
And what happens is: Nothing. The Tableview is not updated at all (section headers stay the same) and the row which should have been changed is in some strange state where it can be selected (the delegate method for row selection is called) but a different row is highlighted. And when I scroll down no new rows are loaded (I can scroll down, but it only shows empty space).
My NSFetchedResultsControllerDelegate is implemented as follows:
- (void) controllerWillChangeContent:(NSFetchedResultsController *)controller
{
[[self tableView] beginUpdates];
}
- (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;
case NSFetchedResultsChangeMove:
case NSFetchedResultsChangeUpdate:
break;
}
}
- (void) controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
switch(type) {
case NSFetchedResultsChangeInsert:
[[self tableView] insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[[self tableView] deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[[self tableView] reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeMove:
[[self tableView] deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
[[self tableView] insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void) controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[[self tableView] endUpdates];
}
I have tried another implementation which collects all change requests, optimizes them and executes all of them within - (void)controllerDidChangeContent:(NSFetchedResultsController *)controller, but it had the same result.
Any help on this would be greatly appreciated.
Thank you very much!
I have a UITableView backed by an NSFetchedResultsController that shows items which have been bookmarked by the user. Items can be un-bookmarked from a button within the row, which leads to the problem. After an item is un-bookmarked, it should disappear from the table view because it no longer matches the predicate, but because my row counts per section have been altered by the update, I get a variation of this error:
CoreData: error: Serious application error. An exception was caught from the delegate 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 (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). with userInfo (null)
Here's my very simple didChangeObject method:
-(void)controller:(NSFetchedResultsController *)controller
didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
[super controller:controller didChangeObject:anObject atIndexPath:indexPath forChangeType:type newIndexPath:newIndexPath];
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}
Is there some way that I can instruct the NSFetchedResultsController not to sweat the mismatched counts? Or do I need a different approach entirely?
Your didChangeObject delegate method looks very incomplete,
in particular it does not check which event occurred (insertion, deletion or update).
You can find a template in the NSFetchedResultsControllerDelegate protocol
documenation.
The method looks normally similar to this:
- (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:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeUpdate:
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
}
}
You should also implement the controller:didChangeSection:atIndex:forChangeType:
delegate method.
And I do not understand what the
[super controller:controller didChangeObject:anObject atIndexPath:indexPath forChangeType:type newIndexPath:newIndexPath];
call is for!
Martin R gave the good answer, but he omit one important thing:
If the FetchControllerProtocol wants to refresh a lot of rows at the same time, it would probably crash.
The Apple Doc give a very clear typical example of the process. It's important to surround your table changes by beginUpdates & endUpdates methods.
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
[self.tableView beginUpdates];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView endUpdates];
}
you should also implement the section refresh if you have sections
- (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;
}
}
Hope this helps
Update: sometimes I get the error message
2013-03-27 19:23:23.094 *** Assertion failure in -[TableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-2372/UITableView.m:1070
2013-03-27 19:23:31.280 [53301:c07] CoreData: error: Serious application error. Exception was caught during Core Data change processing. This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification. 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 (1), plus or minus the number of rows inserted or deleted from that section (1 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)
2013-03-27 19:23:31.281 [53301:c07] *** 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 (4) must be equal to the number of rows contained in that section before the update (1), plus or minus the number of rows inserted or deleted from that section (1 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 have an app which fetches most of it's data from the remote service, and persists it using core data.
I'm writing a view which shows a collection of objects in UITableView with infinite scroll
[Offtopic: One would think that it is fairly typical, but turns out there are at least 20 ways to do that...]
In my viewDidLoad I construct the fetch request, with a fixed small limit, 0 offset, and a fixed batch size. I initiate the NSFetchedResultsController with that request, nil for cache name and nil for the sectionNameKeyPath and call performFetch. Then I attach the data source which takes data from NSFetchedResultsController in a typical way (I only ever have one section)
- (NSInteger) tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section
{
return [[fetchController.resultsController sections][section]
numberOfObjects];
}
- (NSInteger) numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
So far so good, I can see my objects pulled out of DB. The idea is that user sees persisted stuff straight away, until new data comes from network.
Then, inside performBlock on a background ManagedObjectContext (which has main UI ManagedObjectContext as a parent) I perform a network fetch, convert the objects to the core data and push them to the main UI context. That works all right as well.
Trouble starts when NSFetchResultsController starts getting notifications about new objects arriving. I have a typical boilerplate, which is mentioned in several books and in the apple's own documentation:
- (void)controller:(NSFetchedResultsController*)controller
didChangeObject:(id)anObject
atIndexPath:(NSIndexPath*)indexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath*)newIndexPath
{
switch(type) {
case NSFetchedResultsChangeInsert:
[self.table insertRowsAtIndexPaths:#[newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.table deleteRowsAtIndexPaths:#[indexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self.delegate
populateCell:[self.table cellForRowAtIndexPath:indexPath]
indexPath:indexPath
];
break;
case NSFetchedResultsChangeMove:
[self.table deleteRowsAtIndexPaths:#[indexPath]
withRowAnimation:UITableViewRowAnimationFade];
[self.table insertRowsAtIndexPaths:#[newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
Now things start to get weird. Firstly, the newIndexPath I get is strange: sometimes it's 0, sometimes it's 18, sometimes it's 28. It appears to be non-deterministic (my fetchLimit and batchSize are set to 20)
Furthermore, when insertRowsAtIndexPath is called the assertion is thrown from the cocoa layer:
*** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-2380.17/UITableView.m:1070
No, there are no further explanations. Isn't it marvellous?
I take it there is some inconsistency between the number of rows or sections UITableView expects and actually gets. Are there any suggestions in which way should I look? Without UITableView source code I have no idea where to start looking. Currently I'm tempted to re-implement analogue of NSFetchedResultsController myself so that I can see what's actually going on.
did you implement the other delegate methods for the NSFetchedResultController?
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
[self.tableView beginUpdates];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView endUpdates];
}
- (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;
}
}
I'm currently working on a project where I use UITableView and NSFetchedResultsController to store my data. The problem I'm facing is the use of sections is not working.
The entity i want to be displayed as a tables is of the following character:
Channel (String)
Server (String)
Name (String)
And I want to have sections based on the server and the rows in each section is the channel. The layout will look like this:
Server1
Channel1
Channel2
Server2
Channel3
Channel4
etc..
The problem though is that when I try to insert/remove/change an element in the NSFetchedResultsController the application get an exception as the following:
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 (3) must be equal to the number of sections contained in the table view before the update (2), plus or minus the number of sections inserted or deleted (0 inserted, 0 deleted). with userInfo (null)
I have done some research and people say it's because the UITableViewController Delegate fires first and tries to display an element/section that at the time does not exists. I have yet not found a solution and that is why I'm here.
This is my delegate methods for NSFetchedResultsController
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.tableView;
NSLog(#"--- Core Data noticed change");
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;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView endUpdates];
}
Many thanks
Robert
You have to implement also the
controller:didChangeSection:atIndex:forChangeType:
delegate function, otherwise the table view will not be notified of inserted or deleted sections in the Core Data store.
You find a sample implementation in the NSFetchedResultsControllerDelegate Protocol Reference.
I have a standard split view controller, with a detail view and a table view. Pressing a button in the detail view can cause the an object to change its placement in the table view's ordering. This works fine, as long as the resulting ordering change doesn't result in a section being added or removed. I.e. an object can change it's ordering in a section or switch from one section to another. Those ordering changes work correctly without problems. But, if the object tries to move to a section that doesn't exist yet, or is the last object to leave a section (therefore requiring the section its leaving to be removed), then the application crashes.
NSFetchedResultsControllerDelegate has methods to handle sections being added and removed that should be called in those cases. But those delegate methods aren't being called for some reason.
The code in question, is boilerplate:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
NSLog(#"willChangeContent");
[self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
NSLog(#"didChangeSection");
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)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath {
NSLog(#"didChangeObject");
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;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
NSLog(#"didChangeContent");
[self.tableView endUpdates];
[detailViewController.reminderView update];
}
Starting the application, and then causing the last object to leave a section results in the following output:
2011-01-08 23:40:18.910 Reminders[54647:207] willChangeContent
2011-01-08 23:40:18.912 Reminders[54647:207] didChangeObject
2011-01-08 23:40:18.914 Reminders[54647:207] didChangeContent
2011-01-08 23:40:18.915 Reminders[54647:207] *** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-1145.66/UITableView.m:825
2011-01-08 23:40:18.917 Reminders[54647:207] Serious application error. Exception was caught during Core Data change processing: Invalid update: invalid number of sections. The number of sections contained in the table view after the update (5) must be equal to the number of sections contained in the table view before the update (6), plus or minus the number of sections inserted or deleted (0 inserted, 0 deleted). with userInfo (null)
As you can see, "willChangeContent", "didChangeObject" (moving the object in question), and "didChangeContent" were all called properly. Based on the Apple's NSFetchedResultsControllerDelegate documentation "didChangeSection" should have been called before "didChangeObject", which would have prevented the exception causing the crash.
So I guess the question is how do I assure that didChangeSection gets called?
Thanks in advance for any help!
This problem was caused by using a transient attribute as the sectionNameKeyPath. When I instead stored the attribute used for the sectionNameKeyPath in the database, the problem went away. I don't know if there is a way to get the sections updated based on a NSFetchedResultsController content changes when using a transient attribute as a sectionNameKeyPath. For now I am considering this a limitation of transient attributes.
I am doing the same thing in my application (transient property in the sectionNameKeyPath) and am not seeing the problem you are experiencing. I am testing this on iOS 4.2.1... There is a known bug where you cant trust any of the FRC delegate callbacks in iOS 3.X, you have to do a full [tableView reloadData] in the controllerDidChangeContent: message. see the FRC documentation
I have tested going from an existing section with another entry in it to a nonexistent section as well as from a section with only one row to another nonexistent section.