In my app I have a viewcontroller which is the delegate of NSFetchedResultsController and the delegate and datasource of a UITableView.
When an add button in the navigation controller is pressed, the next view is pushed correctly and I can correctly add a new Person entity.
My issue is that when the add button is pressed, a blank row is added to the tableView as the new view is pushed, and is still there after the new entity record is correctly created
This is the target action for the add button:
- (void)addPerson:(id)sender
{
AddPersonViewController *addPersonController = [[AddPersonViewController alloc] initWithNibName:#"AddPersonViewController" bundle:nil];
[addPersonController setPerson:[NSEntityDescription insertNewObjectForEntityForName:#"Person" inManagedObjectContext:self.managedObjectContext]];
[self.navigationController pushViewController:addPersonController animated:YES];
}
The code that creates the blank row (from the apple docs for the NSFetchedResultsControllerDelegate) is here:
- (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;
}
}
Any ideas how to stop this blank row being created?
Calling
[NSEntityDescription insertNewObjectForEntityForName:#"Person" inManagedObjectContext:self.managedObjectContext]
in addPerson creates a new Person object and adds it to the managed object context. If the fetched results controller of the table view is configured to get all Person objects, this will result in a new (blank) table row for this new object.
I don't know how your AddPersonViewController works. If it modifies the object given via setPerson, then the table row should be updated. If it creates a new Person object, then the first (blank) entry will remain.
You should probably delay the creating of the new Person object until the AddPersonViewController has all data to actually create and populate the object. You could do this either by moving the insertNewObjectForEntityForName: call to the AddPersonViewController, or by using a delegate method in your table view controller that is called from the AddPersonViewController.
Related
I am adding a new item to the bottom of a UITableView and after inserting the item, I want the UITableView to scroll to the very bottom to display the newly inserted item. New items are saved to Core Data and the UITableView is automatically updated using NSFetchedResultsController.
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
switch (type) {
case NSFetchedResultsChangeInsert:
NSLog(#"*** controllerDidChangeObject - NSFetchedResultsChangeInsert");
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
//THIS IS THE CODE THAT DOESN'T WORK
[self.tableView scrollToRowAtIndexPath:newIndexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
break;
....
}
This leads to an out of bounds error, I can't seem to make it work. I can scroll to the second to last comment by adjusting the row of the index path, but I can't get to the very last item.
Basically, I'm adding a comment to a table of comments and after a comment is added, I want the table to scroll to the most recent comment.
You need to call endUpdates so that the tableView can calculate its new sections and rows. A simple case would look like this:
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:insertedIndexPath] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];
[self.tableView scrollToRowAtIndexPath:insertedIndexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
As you use NSFetchedResultsController, it is a bit more complicated, as the calls do beginUpdates, insertRowsAtIndexPaths:withRowAnimation:, and endUpdates are typically in different delegate methods. What you could do then is
add a property insertedIndexPath to store the inserted index path
after the -insertRowsAtIndexPaths:withRowAnimation: call in -controller:didChangeObject:atIndexPath:, add
self.insertedIndexPath = insertedIndexPath;
after [self.tableView endUpdates] in -controllerDidChangeContent:, add
if (self.insertedIndexPath) {
[self.tableView scrollToRowAtIndexPath:self.insertedIndexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
self.insertedIndexPath = nil;
}
See if this helps...
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];
[self.tableView scrollToRowAtIndexPath:newIndexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
Admittedly I'm new. Criticism welcomed.
After a month of trying various tweaks, the problem remains. For simplicity: There are three view controllers, calling them A, B, & C.
A has 2 round rect buttons that each do a push segue to B.
B is a tableview filled in by the troublesome 'NSFetchedResultsController' described. B has a Nav bar button (Add) to segue to C which accepts two data fields and saves the new item then pops View C and shows View B.
Adding new item from View C and returning to View B results in a correctly sorted list. But if I return from View B back to View A, then segue again to View B, the list is not sorted correctly. Each time View B appears it creates a new Fetched Results Controller. Back to A, Back to B the view is not sorted correctly. Repeat - same problem. But return back to A, wait 3 seconds, and then segue to B and woohoo the list is sorted correctly. I've tried it with the cache set to nil, and set to a value that is deleted when the view disappears.
Every subsequent new display of View B will show the list correctly sorted. The wrong sort sequence only occurs in the sequence described above - after adding 1 item, going back one view and then redisplaying the table view fed by the FRC.
Questions and suggestions are welcomed. Thanks in advance. The rest of the original question is shown below;
I've a NSFetchedResultsController filling a table view in alphabetic order, with a segue to another view to add an item. When I return to the main table, the sort is random. I can exit from the view to a prior view on the hierarchy and reload the view and it will still be randomly sorted. I can repeat this several times in succession. However, if I wait a few seconds, and follow the same steps, the view will sort correctly. It appears that the correct sort happens when reading from the saved file, which I understand happens "sometime later" automatically. Cache is set to nil.
I have a saveContext call and then a reloadData call to the table view in the method that adds an item, trying to force the data to save and then reload the table. I have the NSFetchedResultsControllerDelegate set to the view.
Any suggestions?
The code for the Fetched Results Controller is as follows:
- (NSFetchedResultsController *)fetchedResultsController {
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"Player"];
[fetchRequest setFetchBatchSize:20];
NSSortDescriptor * lastNameSort = [NSSortDescriptor sortDescriptorWithKey:#"lastName" ascending:YES selector:#selector(localizedCaseInsensitiveCompare:)];
NSSortDescriptor * firstNameSort = [NSSortDescriptor sortDescriptorWithKey:#"firstName" ascending:YES selector:#selector(localizedCaseInsensitiveCompare:)];
fetchRequest.sortDescriptors = [NSArray arrayWithObjects: lastNameSort, firstNameSort, nil];
NSFetchedResultsController * frc = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:nil];
frc.delegate = self;
NSError *error = nil;
if (![frc performFetch:&error]) {
// Handle the error
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
_fetchedResultsController = frc;
return _fetchedResultsController;
}
- (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;
}
}
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
Player * player = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = player.fullName;
}
- (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;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView endUpdates];
}
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;
}
I'm currently developing a holiday journal like application which stores each place you have visited by its type. For instance, a restaurant name should be stored under 'food' section. I have managed to work with core data and create the table with no problem. However, the problem is whenever I try to change the type of a place (thus tableview need to reorder), I would get an error message:
2009-07-22 21:04:58.150 HolidayTest[8662:20b] Serious application error. Exception was caught during Core Data change processing: *** -[NSCFArray removeObjectAtIndex:]: index (4) beyond bounds (4) with userInfo (null)
2009-07-22 21:04:58.151 HolidayTest[8662:20b] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[NSCFArray removeObjectAtIndex:]: index (4) beyond bounds (4)'
I know the fetch results controller's delegate has updated the table. But the problem is the fetch controller has not updated its own section and row data. The easy way is to tell the fetch results controller to refetch its data if user changes its type. However, it's not an efficient way to manage the data.
Thank you for your answer TahoeWolverine. Here is the code where i change the type of a place where user selects a row on a tableview to change its type.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSIndexPath *selectionIndexPath = [NSIndexPath indexPathForRow:selected inSection:0];
UITableViewCell *checkedCell = [tableView cellForRowAtIndexPath:selectionIndexPath];
checkedCell.accessoryType = UITableViewCellAccessoryNone;
[[tableView cellForRowAtIndexPath:indexPath] setAccessoryType:UITableViewCellAccessoryCheckmark];
selected = indexPath.row;
NSManagedObject *aType = [siteType objectAtIndex:indexPath.row];
self.site.type = [aType valueForKey:#"name"];
[tableView deselectRowAtIndexPath:indexPath animated:YES];
[self.delegate TypeSelectionController:self didChangeType:YES];
}
this code corresponds to the fetch results controller's delegate which runs the following code
- (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;
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:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:newIndexPath.section] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id) <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
switch(type)
{
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView endUpdates];
}
then i will the get the error message.......
It is difficult to tell exactly what you mean here, but it seems as though you're attempting to remove beyond the end of your array. It's hard to know what you would be doing that is causing that.
If I were to implement this, I would
A) Remove the entry from the table:
[tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade];
B) Remove the entry from the data structure.
C) Modify the data (title and whatever else).
D) Add the entry where it needs to be in the data structure.
E) Add the entry where it should be in the table:
[tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade];
Hope that helped. I'll keep an eye if you update the question.
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;
...