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.
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!
According to KVO observing of NSMangedObject in a UITableViewCell, one should implement NSFetchedResultsControllerDelegate rather than using KVO to observe propery changes on managed objects fetched with NSFetchedResultsController.
But how can I determined which properties have changed and on which objects?
I have data updates streaming in via a socket updating my managed objects, and want my UI to update accordingly whenever certain properties of listed objects change. I don't want to redraw the whole table, or each cell, only the label that displays the value of the property that changed, for example.
Is this doable, or should I resort to KVO but optimize it somehow?
You should not use KVO for updating CoreData objects. The reason being: the same object record can be retrieved from two separate contexts, and KVO observing would only notify you of the object from your view's context, leaving you unaware of updates made on the same object in a different context. Using the delegate methods from the fetched results controller allows you to respond to changes made on an object from other contexts. Here's example code taken from the 'CoreDataBooks' sample project, and is the proper way to respond to changes using a fetched results controller:
/*
NSFetchedResultsController delegate methods to respond to additions, removals and so on.
*/
- (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;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
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:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationAutomatic];
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 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
I have a gesture on a UITableViewCell subclass called ArticleCell, so when it is swiped a method in the UITableViewController class gets called to delete the cell that was swiped.
The delegate method looks like this:
- (void)swipedToRemoveCell:(ArticleCell *)articleCell {
NSIndexPath *indexPath = [self.tableView indexPathForCell:articleCell];
[self.tableView beginUpdates];
[self.tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView endUpdates];
[self.tableView reloadData];
}
But every time I swipe, I get this error:
Invalid update: invalid number of rows in section 0
More information: It uses Core Data for the data source, so it uses NSFetchedResultsController. Do I have to update something there? (I haven't touched any of its methods.)
You always need to also remove the row from your data source object as well. You will need to remove it from your Core Data store at the same time as you delete the row representing the data from the table view itself.
The issue is this mismatch, you remove the row from the table view but your -numberOfRowsInTableView data source method is still returning the old number of rows because the fetched results controller still sees that number in the data store.
it is happening because when you are deleting the row but you are not deleting the actual object from the list, therefore it is returning wrong number of counts for rows or either for section. you should update your list as well.
One more thing you don't need to reload your data as it is doing already when you deleting the row.
Use this directly by removing that object from array and reloading tableView after that:
- (void)swipedToRemoveCell:(ArticleCell *)articleCell {
NSIndexPath *indexPath = [self.tableView indexPathForCell:articleCell];
[DATASOURCE_ARRAY removeObjectAtIndex:indexPath.row]; // Here DATASOURCE_ARRAY is the array you are using as datasource of tableView
[self.tableView reloadData];
}
Hope it helps you.
Since you are using core data , you can do this
In your delete action call this code
NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
[self.fetchedResultsController.managedObjectContext deleteObject:object];
Overwrite this fetchcontroller delegate ....
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.tableViewIB;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationMiddle];
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;
}
}
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.