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];
}
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!
In my app, I manage a library of music files using CoreData as the backing store. Works pretty well. I also use an NSFetchedResultsController to bind the data to a table view for display, which is also all fine and dandy. The issue arises when I try to enable editing on the table view.
As usual, I return YES from tableView:canEditRowAtIndexPath: to allow rows to be editied, enabling the swipe to delete UI. That works nicely and shows up right. However, after deleting files and whatnot, the UITableView begins having some strange rendering issues:
I don't send any messages to the tableview to explicitly request UI updates, since the fetched results controller notifies my UITableViewController subclass of this, which then sends the appropriate messages:
/**
* Handler for changing an entire section at once
*/
- (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:UITableViewRowAnimationLeft];
break;
}
}
/**
* Handler for changing a single object, usually a row.
*/
- (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:UITableViewRowAnimationRight];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:#[indexPath]
withRowAnimation:UITableViewRowAnimationLeft];
break;
case NSFetchedResultsChangeUpdate:
[tableView reloadRowsAtIndexPaths:#[indexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:#[indexPath]
withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:#[newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
/**
* Called when the fetched results controller begins sending update messages.
*/
- (void) controllerWillChangeContent:(NSFetchedResultsController *) controller {
[self.tableView beginUpdates];
}
/**
* Called when the fetched results controller finishes sending updates.
*/
- (void) controllerDidChangeContent:(NSFetchedResultsController *) controller {
[self.tableView endUpdates];
}
After reading the docs, I'm pretty sure all I need to do is delete the object from the context, which happens like this:
- (void) tableView:(UITableView *) tableView commitEditingStyle:(UITableViewCellEditingStyle) editingStyle forRowAtIndexPath:(NSIndexPath *) indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
// perform on the object context's background queue
[[SQUPersistence sharedInstance].managedObjectContext performBlock:^{
[[SQUPersistence sharedInstance].managedObjectContext deleteObject:[_fetchedResultsController objectAtIndexPath:indexPath]];
// Save database
[[SQUPersistence sharedInstance] save:nil];
}];
}
}
Eventually, the UI enters a strange state as seen above and the table gets out of sync with the backing store, and no animations are performed. Am I doing this entire deletion thing wrong, or have I encountered a bug with Apple's classes?
I suspect it is threading issue. If you're deleting on the background queue, but referencing the object from the main queue, [_fetchedResultsController objectAtIndexPath:indexPath], I am sure that will cause problems.
Right off, I would confirm by trying:
- (void) tableView:(UITableView *) tableView commitEditingStyle:(UITableViewCellEditingStyle) editingStyle forRowAtIndexPath:(NSIndexPath *) indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
NSManagedObjectContext *context = _fetchedResultsController.managedObjectContext;
[context deleteObject:[_fetchedResultsController objectAtIndexPath:indexPath]];
// Save database (You do not have to do this here)
[context save:nil];
}
}
If you do not have issues doing it this way, I'd suggest leaving it. There is likely little benefit in deleting a single object in the background. Where you might hang would be the save operation, which you don't actually need to do here. You can leave the save till user exits or some other point at which any lag would be masked by the UI change.
I want to change a UITableViewCell based on whether or not a file exists in the documents directory. I feel like this should be Notification Based and the notifications should be sent when the objects isAvailable property has changed.
I don't want to create threading problems by accident. Since I am manipulating my core data objects on the main thread, should it be ok to setup a custom setter on my Concrete class to post a notification?
What would be the best way to do this? should I create my own notifications, or should I hook into something that core data already posts?
This is quite simple if you use NSFetchedResultsController. This class can be used in combination with a UITableView to reduce memory overhead and improve response time.
You can find docs at NSFetchedResultsController Class Reference and NSFetchedResultsControllerDelegate Protocol Reference.
In addition, you can implement delegate methods for NSFetchedResultsController. Implementing NSFetchedResultsController delegate methods, it allows you to listen for operations like add, remove, move, or update in your data (the NSManagedObjectContext you registered for) and, hence, in your table.
A very good tutorial on the subject is core-data-tutorial-how-to-use-nsfetchedresultscontroller. Here you can find all the elements to set up a UITableView, a NSFetchedResultsController and its delegate.
Said this, about your question you can use this technique to change the content of a UITableViewCell when isAvailable property (of a specific NSManagedObject) changes. In particular, you should implement the following delegate method to respond to specific changes (see the comment).
- (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: // <---- here you will change the content of the cell based on isAvailable property
[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;
}
}
Hope it helps.
In my app I have UITableView & table data can be updated from remote after user request. I use Core Data & NSFetchedResultsController and when changes took place, -controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: is called:
- (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:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationNone];
break;
case NSFetchedResultsChangeMove:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
All works correctly, except one thing. Sometimes, after reloading the first row, it is displayed without border (border suddenly disappears). What can be wrong?
P.S.: For updating data I create new NSManagedObjectContext in background and manage NSManagedObjectContextDidSaveNotification. When notification comes, mergeChangesFromContextDidSaveNotification is performed on main thread - so updates are made from main thread also.
Most likely you've got a problem in:
tableView:cellForRowAtIndexPath:
… where you provide a cell of the wrong type or size for index zero.
This does not seem to be an issue with core data.
Try calling
[cell.backgroundView setNeedsDisplay];
in order to update the cell. If it does not work, try calling setNeedsDisplay on the tableView.
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.