UITableView isn't updating when didChangeObject is called NSFetchedResultsController - ios

I'm using an NSFetchedResultsController with my UITableView. I successfully receive delegate calls to - (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath but none of the changes I make to insert/update/delete rows in the UITableView actually show up. I even just try to set the background color on the UITableView to see if any changes will show up, but they don't show up unless I push a new view controller and then pop back. Then I'll see the background color and the table updates.
My implementation of the didChangeObject: method is really just the boilerplate template:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
[self.tableView beginUpdates];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView endUpdates];
}
- (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:UITableViewRowAnimationFade];
tableView.backgroundColor = [UIColor redColor];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
tableView.backgroundColor = [UIColor blueColor];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
I added a button in the navbar with an IBAction which just calls [self.tableView reloadData]; and whenever I tap that, then all of the insertions and updates show up in the table. However, they don't show up as the changes happen.
What is wrong?

It looks like the delegate calls to didChangeObject (and the other methods) are not happening on the Main thread which means they can't update the UI, but those changes were just silently being dropped.
I updated the three methods I included above so that the bodies of those methods were all dispatched on the main thread and everything works as expected. Here is an example below:
dispatch_sync(dispatch_get_main_queue(), ^{
[self.tableView beginUpdates];
});

Related

UITableViewAlertForLayoutOutsideViewHierarchy when UITableView doesn't have window iOS13

I started to receive warning (below) on iOS13. I have noticed that this warning pops up because UITableView's window is null (another tab is selected, pushed detailed view controller on table selection...).
I am trying to update UITableView from NSFetchedResultController delegate. What is the correct way to do this on iO13 to keep table updated?
Code below worked fine on previous releases.
PS: Any kind of beginUpdates , reloadRowsAtIndexPaths:withRowAnimation: , insertSections:withRowAnimation: , endUpdates will cause this warning.
PS: I tried reload table but if I navigate back I lose animation to deselect row (clear row selection).
2019-09-27 09:40:42.849128+0200 xxx[63595:9762090] [TableView] Warning
once only: UITableView was told to layout its visible cells and other
contents without being in the view hierarchy (the table view or one of
its superviews has not been added to a window). This may cause bugs by
forcing views inside the table view to load and perform layout without
accurate information (e.g. table view bounds, trait collection, layout
margins, safe area insets, etc), and will also cause unnecessary
performance overhead due to extra layout passes. Make a symbolic
breakpoint at UITableViewAlertForLayoutOutsideViewHierarchy to catch
this in the debugger and see what caused this to occur, so you can
avoid this action altogether if possible, or defer it until the table
view has been added to a window. Table view: ;
layer = ; contentOffset: {0, -64};
contentSize: {375, 3432}; adjustedContentInset: {64, 0, 0, 0};
dataSource: >
// ------------ ------------ ------------ ------------ ------------ ------------
#pragma mark - FetchedResultsController delegate
- (void) controllerWillChangeContent:(NSFetchedResultsController *)controller {
// if (self.tableView.window) {
[self.tableView beginUpdates];
// }
}
- (void) controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
if (type == NSFetchedResultsChangeInsert && newIndexPath != nil) {
[self.tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}
if (type == NSFetchedResultsChangeUpdate && indexPath != nil) {
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
// id<CellLoadable> cell = [self.tableView cellForRowAtIndexPath:indexPath];
// [cell loadData:anObject];
}
if (type == NSFetchedResultsChangeMove && indexPath != nil && newIndexPath != nil) {
// if cell is visible, update it
id<CellLoadable> cell = [self.tableView cellForRowAtIndexPath:indexPath];
[cell loadData:anObject];
[self.tableView moveRowAtIndexPath:indexPath toIndexPath:newIndexPath];
}
}
- (void) controller:(NSFetchedResultsController *)controller didChangeSection:(id<NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
if (type == NSFetchedResultsChangeInsert) {
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
}
if (type == NSFetchedResultsChangeDelete) {
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
}
}
- (void) controllerDidChangeContent:(NSFetchedResultsController *)controller {
// if (self.tableView.window) {
[self.tableView endUpdates];
// }
}
PS: Any kind of beginUpdates , reloadRowsAtIndexPaths:withRowAnimation: , insertSections:withRowAnimation: , endUpdates will cause this warning.
I found that wrapping the table update where the breakpoint triggers in dispatch_async eliminates the issue:
dispatch_async(dispatch_get_main_queue(), ^(void){
[self.table reloadSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:UITableViewRowAnimationFade];
});
(may have to walk up the call stack to find the call when it breaks)
You could try the code below, i.e. by checking for the window and calling reloadData instead. FYI this doesn't actually reload all the cells it just calls the number of rows, sections etc. Then on next appear the new cells will be loaded. You would be better off disabling the fetch controller when the view disappears though and reloading the table on next appear.
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
if(!self.tableView.window){
return;
}
[self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
if(!self.tableView.window){
return;
}
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 {
UITableView *tableView = self.tableView;
if(!tableView.window){
return;
}
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeMove:
[tableView moveRowAtIndexPath:indexPath toIndexPath:newIndexPath];
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] withEvent:anObject];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
if(!self.tableView.window){
[self.tableView reloadData];
return;
}
[self.tableView endUpdates];
}
Note, you also might like to reselect a previously selected cell. There are a few different ways to do that.

UITableViewCells are flickering when tableview first loads

So I have a UITabelView using custom UITableViewCells. I chased down the issue and its in this method "controllerDidChangeContent", which is getting called about 5 times as the table loads. This method is being called because of Core Data updates.
I can stop the flickering from happening if I comment out either line that has <--- pointing at it. I am sure we do NOT want to comment out the [self reloadData] as that refreshes the data when Core Data is changed. But I am not so sure if endUpdates is ok to comment out?
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[[self tableView] endUpdates]; <--
[[self tableView] reloadData]; <--
[UIView dismissIndicator];
}
However, if I comment out the endUpdates line then all the table data is NOT getting written to the UITableView. There is a couple cells missing.
I am thinking the issue isn't this method or its inner methods, but rather how the cell content is being drawn?
You should not need to call reloadData after the endUpdates. You make changes to your UITableView data source within the beginUpdates endUpdates block so that you don't have to call reloadData. You call reloadData if your entire data source has changed because it can be relatively expensive call. Using beginUpdates and endUpdates gives you the ability to change/insert/delete an item from your data source without reloading the entire UITableView.
Here is what it should look like (taken from Ray Wenderlich tutorial):
- (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:[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)controller:(NSFetchedResultsController *)controller didChangeSection:(id )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 {
// The fetch controller has sent all current change notifications, so tell the table view to process all updates.
[self.tableView endUpdates];
}
If this does not help solve your problem then I will need to see more code, specifically where you are performing the data source updates within the beginUpdates and endUpdates block.
Why don't you simply call reloadSections method instead of [self.tableView reloadData];
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationNone];

iOS: Debugging simple UI lags (based on CoreData I guess)

I'm testing my app currently especially on an iPhone 4s. And some simple UI tasks seem laggy and delayed, sutch as selecting an image in a collection view or changing the state of a UI checkbox. Is there some better way to do this? Do I need to do here something asynchronous? If something is changed an attribute in CoreData gets changed, but can this be that slow?
CollectionView (Image names are all saved in an array on viewdidload):
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *identifier = #"IconCell";
IconSelectionCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath];
cell.iconImageView.image = [UIImage imageNamed:[self.icons objectAtIndex:indexPath.row]];
return cell;
}
#pragma mark Collection View Delegate methods
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
self.mainCategory.icon = [self.icons objectAtIndex:indexPath.row];
}
And the UISwitch for example:
- (IBAction)liveBudgetSwitched:(id)sender
{
self.spendingCategory.liveBudget = [NSNumber numberWithBool: self.liveBudgetSwitch.on];
}
With that code is hard to help you. It can be so many things. Maybe your images are way bigger than they should be, maybe your cell has shadow in one label or in the background, who knows?
Therefore, you need to find out the problem first.
In this case you can open instruments and analyze if the problem is CPU or GPU.
You can know if the problem is CPU using the Time Profiler and for the GPU you can use Core Animation.
In the time profiler try to check where the most CPU is being spent. You can use some options to help you focus on your code. You can show objective-c only and hide system libraries.
In Core Animation you have a bunch of options that you can use to check some problems you might have. This options are:
Color Blended Layers - Highlights where you have several layers on top of each other and the GPU needs to blend those
Color hits green and misses red - If it is green you are fine, if it is red then you aren't caching your images and those images are being regenerated a lot of times (it can happen when you have shadows, for instance)
Color copied images - if it shows blue then the core animation is sending a copy of the image to the render server when it should be sending a pointer
Flash Updated Regions - it shows yellow when the view is redrawn
Yes I did. And the lags are due to the NSFetchedResultsController constantly monitoring even when my view is not visible. I just used my property I've used for all user driven changes in my table view to stop the controller from listening when my view is not on the screen:
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
self.suspendAutomaticTrackingOfChangesInManagedObjectContext = YES;
}
-(void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
self.suspendAutomaticTrackingOfChangesInManagedObjectContext = YES;
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
self.suspendAutomaticTrackingOfChangesInManagedObjectContext = NO;
}
#pragma mark - NSFetchedResultsControllerDelegate
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
if (self.suspendAutomaticTrackingOfChangesInManagedObjectContext) return;
[self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller
didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex
forChangeType:(NSFetchedResultsChangeType)type
{
if (self.suspendAutomaticTrackingOfChangesInManagedObjectContext) return;
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
{
if (self.suspendAutomaticTrackingOfChangesInManagedObjectContext) return;
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:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeMove:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
if(self.suspendAutomaticTrackingOfChangesInManagedObjectContext) return;
NSLog(#"Controller did change content");
[self.tableView endUpdates];
}

tableView:cellForRowAtIndexPath called with nil indexPath after deleting item

I have a rather vanilla UITableView managed by an NSFetchedResultsController to display all instances of a given Core Data entity.
When the user deletes an entry in the table view by swiping over it, tableView:cellForRowAtIndexPath: eventually gets called on my UITableViewController with a nil indexPath. Since I had not expected it to be called with a nil indexPath, the app crashes.
I can work around the crash by checking for that nil value and then returning an empty cell. This seems to work, but I still worry that I may have handled something wrong. Any ideas? Has anybody ever seen tableView:cellForRowAtIndexPath: called with a nil indexPath?
Note that this only happens when the user deletes from the table view by swiping over the cell. When deleting an item using the table view editing mode, it doesn't happen. What would be different between the two ways to delete a cell?
So is it really an OK situation to get a nil indexPath in a table view delegate method?
My view controller code is really standard. Here is the deletion:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete) {
// Delete the row from the data source
NSManagedObject *managedObject = [self.fetchedResultsController objectAtIndexPath:indexPath];
[self.moc deleteObject:managedObject];
NSError *error = NULL;
Boolean success = [self.moc save:&error];
if (!success) { <snip> }
// actual row deletion from table view will be handle from Fetched Result Controller delegate
// [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
} else { <snip> }
}
This will lead to the NSFetchedResultsController delegate method being called:
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeInsert: <snip> break;
case NSFetchedResultsChangeUpdate: <snip> break;
case NSFetchedResultsChangeMove: <snip> break;
}
}
And of course, the data source methods are handled by the NSFetchedResultsController, e.g.:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
}
Many thanks.
It seems like you are deleting the indexPath from table but table data source is not updating.
Did you verify the data source udation process by NSFetchedResultsController is correctly updationg the table data source.?
I would do like this, since you are populating the table directly from your managed context, why not on delete first delete the object from the managed context and then imediately update the table from the context using reloadData. But using your approach i think you need to add beginUpdates and endUpdates:
#pragma mark -
#pragma mark Fetched results controller delegate
- (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)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath {
UITableView *tableViews = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableViews insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableViews deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[_delegate configureCell:[tableViews cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[tableViews deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableViews insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView endUpdates];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[self.tableView scrollToRowAtIndexPath:indexPath
atScrollPosition:UITableViewScrollPositionMiddle
animated:NO];
}
I use CoreData in table views and allow the users to delete records and update them all the time, I have them in 4 apps without encountering the problem you mentioned.
I have these FetchedResultsController Delegate Method in my code
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
// The fetch controller is about to start sending change notifications, so prepare the table view for updates.
[self.myTableView beginUpdates];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
// The fetch controller has sent all current change notifications, so tell the table view to process all updates.
[self.myTableView endUpdates];
}

Best way to select a row after inserting it?

I'm working on an iOS app using the Navigation based, CoreData template.
I would like to select and "scroll to visible" a row after it was inserted into the table view. Ideally i'd like to select it, deselect it and select it again, in order to get a kind of flashing effect.
As i am using the method, that the template provaides, namely:
#pragma mark - Fetched results controller delegate
- (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)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:UITableViewRowAnimationTop];
[self.tableView selectRowAtIndexPath:newIndexPath animated:YES scrollPosition:UITableViewScrollPositionMiddle];
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 am a bit confused and don't know, where to put that selection code.
If i put a
[tableView selectRowAtIndexPath:<#(NSIndexPath *)#> animated:<#(BOOL)#> scrollPosition:<#(UITableViewScrollPosition)#>]
into
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller,
it selects the row, but deselects it immediately and the scrolling doesn't behave as it should either.
in method controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:
change this section:
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView scrollToRowAtIndexPath:newIndexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES];
insertedIndexPath = newIndexPath; //remember for selection
break;
and add method from UIScrollViewDelegate:
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
[self.tableView selectRowAtIndexPath:insertedIndexPath animated:YES scrollPosition:UITableViewScrollPositionNone];
}
remember to add insertedIndexPath variable.
I solved similar problem. I inserted item to beginning of Table and I wanted to select it just right after.
Step 1. - Create a switch to keep state if it shall select first row
#implementation MyTableViewController {
BOOL _selectFirstRow;
}
Step 2. - Switch it to YES while inserting
- (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];
_selectFirstRow = YES;
break;
Step 3. - Create a method to select first row
- (void) selectFirstTableRow
{
[self.tableView selectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]
animated:YES
scrollPosition:UITableViewScrollPositionTop];
}
Step 4. - Call the method to select row right after table is updated
- (void) controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView endUpdates];
if (_selectFirstRow) {
[self selectFirstTableRow];
_selectFirstRow = NO;
}
}
That's all folks .. if you want select different row, store newIndexPath in class parameter too. That's all. I hope it will help you. Cia!
Mostly same answer as above but with corrected placement of when to call scrollToRowAtIndexPath
in method controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:
change this section:
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
//save new index path into your own ivar
self.newlyInsertIndex = newIndexPath
break;
if using boiler plate FRC code, cannot scroll to newly inserted index until table updates finished
Do this in method - (void)controllerDidChangeContent:
if (!self.changeIsUserDriven){
[self.tableView endUpdates];
if (self.newlyInsertedIndex){
//ScrollPositionNone == use least amount of effort to show cell
[self.tableView scrollToRowAtIndexPath:self.newlyInsertedIndex
atScrollPosition:UITableViewScrollPositionNone animated:YES];
//cleanup
self.newlyInsertedIndex = Nil;
}
}
and add method from UIScrollViewDelegate:
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
[self.tableView selectRowAtIndexPath:insertedIndexPath animated:YES scrollPosition:UITableViewScrollPositionNone];
}
remember to add newlyInsertedIndex ivar
This all did not work for me. What I did was:
when I create a new object, I remember it in an instance variable _newInstance
when the object is saved or the creation cancelled, I clear it: _newInstance = nil;
in controllerDidChangeObject I select the row:
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView endUpdates];
if (_newObject) {
NSIndexPath *ip = [self.fetchedResultsController indexPathForObject:_newObject];
[self.tableView selectRowAtIndexPath:ip animated:YES
scrollPosition:UITableViewScrollPositionMiddle];
}
}

Resources