I have a little problem with my UITableViewController or NSFetchedResultsController. I am not sure which is the problem soure but I guess its the UITableViewController.
As I said I use a NSFetchedResultsController to fill my data into a UITableViewController. The data is sorted by date and is also displayed in sections by date-year, e.g. May 2010/June 2010/ and so on. This is displayed as the section header.
Now when I add new data, it automatically uses the current date as default, but if I want to use a date, that is currently not in the section list, e.g. April 2010 (currently May and June in the list), then this is not displayed correctly. Only when I quit the app and start it again it will show the new section headers and everything is fine.
So I somehow need to be able to refresh my list or update it. By just adding it to the context and changing it does not seem to update it.
Again, I am not sure what I need to update, if it is my NSFetchedResultsController object or the UITableViewController.
UPDATE #1:
I just figured that there is an exception in this method :
- (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];
}
This is where I move the data :
- (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];
// Reloading the section inserts a new row and ensures that titles are updated appropriately.
[tableView reloadSections:[NSIndexSet indexSetWithIndex:newIndexPath.section] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
This method is pretty much taken from the CoreDataBooks example from Apple. And this is the error :
Serious application error. An
exception was caught from the delegate
of NSFetchedResultsController during a
call to -controllerDidChangeContent:.
*** -[NSMutableArray removeObjectAtIndex:]: index 1 beyond
bounds [0 .. 0] with userInfo (null)
And after that, the edit mode (where the delete button appears) does not work anymore.
Thanks.
Ok, looks like I found a solution, I just skip these
[self.tableView beginUpdates];
[self.tableView endUpdates];
and always do this.
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView reloadData];
}
I will obviously give this some more logic, since the problem only occures when I want to move my data in a section that does not exist before. Until someone can give me a real solution, I will go with this one.
Adopting the fix by Apple in iPhoneCoreDataRecipes in version 1.2, the proper thing to do is to replace
// Reloading the section inserts a new row and ensures that titles are updated appropriately.
[tableView reloadSections:[NSIndexSet indexSetWithIndex:newIndexPath.section] withRowAnimation:UITableViewRowAnimationFade];
with
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
(FileMerge is your friend!)
Note, there's another fix in version 1.2 that prevents erratic crashing when creating new entries. I had reported this and it was fixed within two weeks. I encourage you to file bug reports with Apple. They're far more responsive than you think, and you can expect to get the best possible fix.
You need to implement the controller:didChangeSection:atIndex:forChangeType: delegate methods. In it, send insertSections:withRowAnimation:, deleteSections:withRowAnimation:, and reloadSections:withRowAnimation: messages to your table view depending on the reported change type.
Related
I've got a view with a TableView and an NSFetchedResultsController.
I'm using a ASINetworkQueue (subclass of NSOperationQueue) and a subclass of ASIHTTPRequest (which in turn is a subclass of NSOperation) to download a JSON feed, parse it and insert corresponding entities into Core Data.
Therefore, in the ASIHTTPRequest subclass, I've got a second NSManagedObjectContext to keep everything threadsafe and nice.
Everything is fine, my background fetching/import fires of each 10 seconds or so, new entities are created and saved into the Core Data store. The NSNotification propagates its way to the ViewController and the NSFetchedResultsController and new rows appear in the TableView.
The problem occurs when the JSON contains an entity with a new value of the section key (lets call it "sectionID") – for example sectionID == 2 instead of sectionID == 1 (you get it?).
At this point, the NSFetchedResultsController should make the table view create a new section, but instead I'm getting an exception:
Serious application error. Exception was caught during Core Data change processing. This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification. *** -[NSArray initWithObjects:count:]: attempt to insert nil object at objects[0] with userInfo (null)
Assertion failed: (_Unwind_SjLj_Resume() can't return), function _Unwind_SjLj_Resume, file /SourceCache/libunwind/libunwind-24.1/src/Unwind-sjlj.c, line 326.
Here is my code for the NSFetchedResultControllers delegate methods:
-(void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
[[self eventTable] beginUpdates];
}
-(void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
NSIndexSet* set = [NSIndexSet indexSetWithIndex:sectionIndex];
switch (type) {
case NSFetchedResultsChangeInsert:
[[self eventTable] insertSections:set withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[[self eventTable] deleteSections:set withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
-(void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
UITableView* tv = [self eventTable];
switch (type) {
case NSFetchedResultsChangeInsert:
[tv insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tv deleteRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self tableView:tv configureCell:[tv cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
}
}
-(void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[[self eventTable] endUpdates];
}
Any thoughts about whats causing the exception? Thanks in advance!
UPDATE 2011-02-03
Not really sure whether the error occurs when a new section is created, or an old one is deleted. I almost think that this occurs when all the rows in a section is deleted, but by some reason the controller:didChangeSection:atIndex:forChangeType is not being called.
Anyone with experience of something similar?
UPDATE 2011-02-08
I think I solved it. The problem was that some extra conditions had to be taken into concern when determining whether to delete the row/section or not.
When using the code supplied in the Apple documentation (along with a few adjustments to make it work in my view), it runs OK.
I think I solved it. The problem was that some extra conditions had to be taken into concern when determining whether to delete the row/section or not. When using the code supplied in the Apple documentation (along with a few adjustments to make it work in my view), it runs OK.
UPDATE 2011-03-22
Basically I used the same approach as in SafeFetchedResultsController.
I don't know if this is the reason for your error but you are missing the NSFetchedResultsChangeMove case in your delegate methods.
case NSFetchedResultsChangeMove:
[changedTableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[changedTableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]withRowAnimation:UITableViewRowAnimationFade];
break;
I have table view which is in a view controller that inherits from UITableViewController.
I use a NSFetchedResultsController that I use to fetch the table data from my core data store. I've tried it with both caching on and off.
I set the delegate of the NSFetchedResultsController to be self and I've implemented controllerDidChangeContent in my view controller.
I've always implemented the table view delegate functions including commitEditingStyle which is called when the user deletes a row.
Here is what happens. The user swipes and deletes a row and commitEditingStyle is called as expected. In that function, I modify my core data objects to so that they are effectively deleted from results. In other words, if you ran the query I passed NSFetchedResultsController again, that row would now no longer be in the results set.
This alone is not enough, so I added a called to tableView reloadData at the end of commitEditingStyle. Still no luck, so I added a refresh button that calls reloadData and that doesn't help either. Going to another view controller (by hitting "back") and returning to the page doesn't usually work, but eventually it figures it out and the row will disappear correctly. Rerunning the program from scratch always works, of course, but how can I get my tableView to update correctly? controllerDidChangeContent never gets called by the way. I thought reloadData would be enough, but it doesn't seem to be.
Have you implemented the Fetched results controller's controller didChangeObject: delegate method?
The easiest way is to create a new project using navigation based template with core-data on and compare the RootViewController with your code. RootViewController already contains the code to perform the update after tableview modification.
It is something like this:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
// Delete the managed object for the given index path
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
[context deleteObject:[self.fetchedResultsController objectAtIndexPath:indexPath]];
// Save the context.
NSError *error = nil;
if (![context save:&error]) {
/*
Replace this implementation with code to handle the error appropriately.
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}
}
Then use this method to update the table view
- (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;
}
First of all, sorry for the too long question.
I know that there are few questions here that discuss similar issues but none of these talks about NSFetchedResultsController with delegate together with update in separate thread. And none of the solutions has helped me.
These are the existing questions:
NSFetchedResultsController: using of NSManagedObjectContext during update brings to crash
Determining which core Data attribute/property change triggered a NSFetchedResultsController update
Core Data executeFetchRequest throws NSGenericException (Collection was mutated while being enumerated)
Collection was mutated while being enumerated. How to determine which set?
etc.
Now about my problem:
I have a separate thread that updates the core data objects from the web (using a socket).
There are few view controllers that display the data from the same core data object (each tab contains a view controller that displays its filtered data).
Each view controller has its own instance of NSFetchedResultsController and the delegate is set to self.
Sometimes I receive was mutated while being enumerated exception on updating the data in the separate thread and sometimes it crashes the app.
I have done many code manipulations in order to try to fix it and seems that nothing helps.
I have tried not to use the managed object directly from table view datasource methods. Instead of that I have created an array which holds a list of dictionaries. i fill those dictionaries in the didChangeObject method from above. This way I don't touch the managed objects at all in the view controller.
And then I have understood that the problem is in NSFetchedResultsController that, probably, iterates the data all the time. And this is the object that conflicts with my data update in the separate thread.
The question is how can I update the core data objects in the separate thread once I have a NSFetchedResultsController with delegate (meaning that it "watches" the data and updates the delagate all the time).
NSFetchedResultsControllerDelegate implementation:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
[self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
if ( self.tabBarController.selectedIndex == 0 ) {
UITableView *tableView = self.tableView;
#try {
switch(type)
{
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView reloadSections:[NSIndexSet indexSetWithIndex:newIndexPath.section] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
#catch (NSException * e) {
NSLog(#"Exception in didChangeObject: %#", e);
}
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
if ( self.tabBarController.selectedIndex == 0 ) {
#try {
switch(type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
#catch (NSException * e) {
NSLog(#"Exception in didChangeSection: %#", e);
}
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView endUpdates];
}
In the table view datasource methods I work directly with the managed object.
Two separate questions here. First, if you are getting mutating errors that means you are mutating a set or array (or relationship) while iterating over that set/array/relationship. Find where you are doing that and stop doing it. That is the only solution.
As for your updates. Your background NSManagedObjectContext should be saving periodically. Your main thread should be listening for NSManagedObjectContextDidSaveNotification and when it receives one it calls the main NSManagedObjectContext on the main thread (as the notification will most likely come in on the background thread) via -mergeChangesFromContextDidSaveNotification: which takes the NSNotification as a parameter. This will cause all of your NSFetchedResultController instances to fire their delegate methods.
Simple as that.
Update
ank you for your reply. The exception is thrown on updating the NSManagedObjectContext in the background thread. I use the same NSManagedObjectContext in both threads. The app should be as close as possible to real time app - the updates constantly and the tables should be updated immediately. I don't save at all - I only update the NSManagedObjectContext. I have seen in one of the questions mentioned that someone used to separate instances of NSManagedObjectContext but he still receive the same exceptions once he merges the changes. So, you suggest using 2 separate NSManagedObjectContext's?
First, read up on multi-threading in Core Data from Apple's documentation (or my book :).
Second, yes you should have one context per thread, that is one of the golden rules of Core Data and multi-threading (the other is don't pass NSManagedObject instances across threads). That is probably the source of your crash and if it isn't it is going to be the source of a crash in the future.
Update
I have tons of data and I update only the modified/new/deleted items in the table. If I will start saving then will it harm the performance?
No, only the updates will be propagated across the threads. The entire data store will not be re-read so it will actually improve performance when you break up the saves into smaller chunks because you will be on the main thread, updating the UI, in smaller chunks so the UI will appear to perform better.
However, worrying about performance before the app is completed is a pre-optimization that should be avoided. Guessing at what is going to perform well and what won't is generally a bad idea.
I'm working on modifying some existing heavy-handed code that simply calls [tableView reloadData] on any change, to using more specific table updates with the insert/delete methods.
However, I'm getting some really bad behavior in doing so. Previously, as one would imagine, when the table loaded, it only requested cells for the rows that were visible at the time. This was the behavior when reloadData was used.
Now that insertSections is being called, all cells are requested after that update, which can be hundreds. This results in cells being created for every row, completely ruining the reusable cell queue and just being all around wasteful. I must be doing something wrong.
The change is this simple, code that results in the tableView asking only for visible rows:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
// ... ensure it's the right key
[tableView reloadData];
}
Code that results in the tableView asking for everything:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
// ... ensure it's the right key
NSUInteger sectionCount = [self sectionCount];
NSIndexSet *indices = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionCount)];
[tableView insertSections:indices withRowAnimation:UITableViewRowAnimationFade];
}
I can toggle back and forth to see the behavior change. Frustrating. Ideas?
Adding a bounty just to see if anyone has any more insight.
The beginUpdates/endUpdates doesn't affect anything, and I wouldn't expect it to, this is just one command, there's nothing extra to coalesce into a single update.
I'm thinking this is simply a side effect of desiring the animation. To have everything "slide" in, it has to have everything to render. Game over.
It appears you are telling the table to insert all of the sections, which is basically equivalent to a reload. When you tell the table it needs to load a section it needs to read all the items in that section. Also, you are doing it outside of a beginUpdates/endUpdates block, which means every time you add in the section the table has to immediately commit the changes. If you wrap everything inside of a beginUpdates/endUpdates it will suspend all the queries until it is done which will allow the system to coalesce redundant queries and eliminate queries that turn out not to be necessary.
You should only be calling insertSections with new sections that are added. If you do that then tableview does not have to query the information of all the unaltered sections, even if they move around. Also, if elements inside an section change but the section itself doesn't you should use the row version of the methods to modify those specific rows inside of the section, and only those rows will get queried.
Below is an example (from a CoreData based app) that should get the point of across. Since you have a custom model instead an NSFetchedResultsController you will have to figure out the type of action going on instead of just checking a bunch of constants handed to you by the system.
//Calculate what needs to be changed
[self.tableView beginUpdates];
for (i = 0 i < changeCount; i++) {
type = changes[i];
switch(type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
switch(type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:(id)[tableView cellForRowAtIndexPath:indexPath]
atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:newIndexPath.section]
withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
[self.tableView endUpdates];
The docs for NSFetchedResultsControllerDelegate provide the following sample code
- (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 reloadSections:[NSIndexSet indexSetWithIndex:newIndexPath.section] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
When I create a new NSManagedObject, NSFetchedResultsChangeInsert fires (great!). When I change the value of an attribute (used for the cell's title), the NSFetchedResultsChangeUpdate fires. Unfortunately, the new title doesn't automatically display unless I reload the table, section or row. Indeed, if the new name causes the result set to sort differently, then NSFetchedResultsChangeMove fires and all is well since the provided code reloads the entire section.
UITableView has a method reloadRowsAtIndexPaths:withRowAnimation so I tried using this under the NSFetchedResultsChangeUpdate code block. It does indeed work ... but the docs for this specific method read as though I don't need it (notice the last line):
Reloading a row causes the
table view to ask its data source for
a new cell for that row. The table
animates that new cell in as it
animates the old row out. Call this
method if you want to alert the user
that the value of a cell is changing.
If, however, notifying the user is not
important—that is, you just want to
change the value that a cell is
displaying—you can get the cell for a
particular row and set its new value.
And yes, if I log what is happening, when
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
gets invoked on an NSFetchedResultsChangeUpdate, it is able to retrieve the latest 'name' value and set it in the cell's textLabel. The name is just not rendering in the cell unless I reload it. Even if I simply click the cell the name shows up. Note that to recreate this behavior, you must create a new managed object and then give it a name that causes it to sort FIRST in the NSFetchedResultsController. That way, the NSFetchedResultsChangeMove doesn't fire (which does work since it reloads the section).
Am I missing something or is this expected behavior? The 'discussion' for reloadRowsAtIndexPaths leads me to believe I should be able to simply set the cell's textLabel without reloading the row, section or table.
You should call [cell setNeedsLayout] or/and [cell setNeedsDisplay] to cause the cell to get refreshed, depending on your cell's implementation.
If you compose the cell of subviews as we usually do, you rely on - layoutSubviews, so you should call [cell setNeedsLayout].
If you draw the cell directly with – drawRect:, you should call [cell setNeedsDisplay].
If you use both composition and drawing, you should call both.
While it's true that you do not need to reload the cell in order to have the changes take place, you have to remember that the iPhone caches view drawing as much as possible. Once you have newly configured your cell, you need to call setNeedsDisplay on the cell in order to trigger the redraw.
Although it's not been explicitly stated, you've actually got to make sure that you include the delegate methods for controllerWillChangeContent: and controllerDidChangeContent:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
[self.tableView beginUpdates];
}
and
- (void)controllerDidChangeContent:(BSFetchedResultsController *)controller {
#try {
[self.tableView endUpdates];
}
#catch (NSException * e) {
NSLog(#"caught exception: %#: %#", [e name], [e description]);
}
#finally { }
}
The NSFRC can fire off multiple controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: methods during any one change, so don't expect the table row to update straight after each one of those. They'll only update after you call endUpdates on the table.
Calling the UI updating logic in MAIN Thread solved my issue (both [cell setNeedsLayout] & [cell setNeedsDisplay] not works for me):
...
case NSFetchedResultsChangeUpdate:
dispatch_async(dispatch_get_main_queue(), {
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
});
break;
...
What's more (not about this issue, but will be useful), you'd better choose to use the newIndexPath if it's available:
...
case NSFetchedResultsChangeUpdate:
dispatch_async(dispatch_get_main_queue(), {
NSIndexPath * targetIndexPath = (newIndexPath ?: indexPath)
[self configureCell:[tableView cellForRowAtIndexPath:targetIndexPath]
atIndexPath:targetIndexPath];
});
break;
...