Scroll to last UITableViewCell using scrollToRowAtIndexPath:atScrollPosition:animated using NSFetchedResultsControllerDelegate methods - iphone

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];

Related

How to prevent NSFetchedResultsController from updating tableview when the controller disappears?

I have a UITabbar with multiple controllers in it. One of the controllers is used to add Events to Core Data, while another controller is used to display events as in a UITableView using NSFetchedResultsController.
Here's the behaviour that I would like to achieve:
Upon disappearing, the UITableView stops updating, and when the user comes back, the entire table view is reloaded. Otherwise, inserting events from the other controller takes longer, as new rows are created in the UITableView, even thought it is not visible.
I'm wondering how I can achieve this behavior, as it doesn't seem to work as I expect it would be:
I have set the delegate of the NSFetchedResultsController to nil in viewWillDisappear, and restore it in viewWillAppear, along with a call to [UITableView reloadData];
Somehow, I do not see the new data, and suspect this is due to the way NSFetchedResultsController stops fetching if it does not have a delegate.
How can I properly "suspend" updates to UITableView when it disappears, but still able to see the entire dataset when the controller reappears?
Try sending performFetch: to the NSFetchedResultsController in viewWillAppear: after you have set its delegate back to self.
I think you do not have to "suspend" the table view updates. A UITableView will anyway only request data from the NSFetchedResultsController for visible cells. If the the table view is not visible, no updates will be fired.
Did you test if inserting events from another controller really takes longer? I doubt it. What does Instruments say?
If your delegate methods are fired, you could still check if the table view is visible before doing any updates.
After that, you do exactly as suggested by rob: do a performFetch: in viewWillAppear:.
Instead of setting the delegate of the NSFetchedResultsController to nil in viewWillDisappear, try setting an object of NSFetchedResultsController to nil
What about this crude approach? Not tested it.
#property (nonatomic) BOOL bruteForceReload;
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.bruteForceReload = NO;
}
-(void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
self.bruteForceReload = YES;
}
-(void)setBruteForceReload:(BOOL)bruteForceReload {
_bruteForceReload = bruteForceReload;
if (_bruteForceReload) {
[self.tableView reloadData];
}
}
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
if (!self.bruteForceReload) {
[self.tableView beginUpdates];
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
if (!self.bruteForceReload) {
switch(type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
default:
return;
}
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
if (!self.bruteForceReload) {
UITableView *tableView = self.tableView;
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
{
if (!self.bruteForceReload) {
[self.tableView endUpdates];
} else {
[self.tableView reloadData];
}
}

NSFetchedResultsController add entity inserts blank row in tableView

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.

How to refresh a UITableViewController or NSFetchedResultsController?

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.

UITableView is getting interaction when changing rows with animation

I have a tableview on a nib file with the interaction setting turned off. I'm animating a section change like this:
[myTableView beginUpdates];
[myTableView deleteSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:YES];
[myTableView insertSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:YES];
[myTableView endUpdates];
The problem is that, when I do this, the rows become selectable. How do I keep the interaction disabled while keeping the animation?
use this
self.tableView.userInteractionEnabled=NO;

reorder fetch results controller for UITableView

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.