NSFetchedResultsControllerDelegate and Alternating Table View Cells - iphone

I have a UITableView that updates from a NSFetchedResultsController. The UITableView has alternating row colours for even and odd rows. I need to add support for the insertion and deletion of objects, so I implemented the NSFetchedResultsControllerDelegate to handle this. However, now my alternating colour scheme fils after inserting anywhere but at the end. Any ideas on how to solve this without reloading the table view (and not loose my animations)?
Here is my code thus far:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
CustomTableViewCell *cell = ...;
indexPath.row % 2 == 0 ? [cell odd] : [cell even];
return cell;
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)object
atIndexPath:(NSIndexPath *)deleteIndexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)insertIndexPath
{
switch(type)
{
case NSFetchedResultsChangeInsert:
[self.mainTableView insertRowsAtIndexPaths:[NSArray arrayWithObject:insertIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.mainTableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:deleteIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
}
}

The followings are what I did for one of my projects:
- (void)reloadRowsFromIndex:(NSUInteger)index {
NSMutableArray *indexPaths = [[NSMutableArray alloc] init];
for (NSInteger i = index; i < [items count]; i++) {
[indexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]];
}
[tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade];
[indexPaths release];
}
reloadRowsFromIndex: is called within tableView:commitEditingStyle:forRowAtIndexPath:, with the first index path being changed.

Related

Resizing the height of UILabel with the height of tableViewCell height

I am new to iOS development. I am using tableView with of dynamic height. The height of tableViewCell increases or decreases on the click for this I am using this code...
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if ([self.selectedPath isEqual:indexPath])
{
return 250;
}
else
{
return 44;
}
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
onSelectCount++;
self.selectedPath = indexPath;
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation: UITableViewRowAnimationLeft];
self.selectedRowIndex = [NSNumber numberWithInteger:indexPath.row];
[self.tableView1 deselectRowAtIndexPath:indexPath animated:YES];
//First we check if a cell is already expanded.
//If it is we want to minimize make sure it is reloaded to minimize it back
if( onSelectCount==1 )
{
NSLog(#"num=%d",onSelectCount);
NSLog(#"First Condition");
[tableView beginUpdates];
NSIndexPath *previousPath = [NSIndexPath indexPathForRow:self.selectedRowIndex.integerValue inSection:0];
self.selectedRowIndex = [NSNumber numberWithInteger:indexPath.row];
self.selectedPath=indexPath;
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:previousPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView endUpdates];
}
if(self.selectedPath.row!=indexPath.row)
{
[tableView beginUpdates];
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:selectedPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView endUpdates];
selectedPath=indexPath;
onSelectCount=0;
[self tableView:tableView didSelectRowAtIndexPath:selectedPath];
}
if(self.selectedRowIndex.integerValue == indexPath.row && onSelectCount==2)
{
[tableView beginUpdates];
self.selectedRowIndex = [NSNumber numberWithInteger:-1];
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
onSelectCount=0;
[tableView endUpdates];
}
}
Now I want to add some label to show some information on the tableViewCell but when I click on the cell it resizes perfectly but label on the cell does not resize.Please tell me how can I resize the UILabels with the Cell height. Any help will be appreciated...
Use the method - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath to achieve this. It is also good to move the whole logic for changing the height for the cells from didSelectRow to cellForRowAtIndexPath.
To answer your question:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
CGRect sizeRect = cell.textLabel.frame;
sizeRect.size.height = cell.contentView.frame.size.height;
cell.textLabel.frame = sizeRect;
return cell;
}
where textLabel is the name of your label.
The basic idea is that you take the frame of the label, then set it's height to the height of cell's view and then set the new size to the label.

UITableView deleteRowsAtIndexPath crash when delete last record

I'm encountering the following error when I delete the last record from a UITableView.
Terminating app due to uncaught exception
'NSInternalInconsistencyException', reason: 'Invalid update: invalid
number of rows in section 0. The number of rows contained in an
existing section after the update (3) must be equal to the number of
rows contained in that section before the update (1), plus or minus
the number of rows inserted or deleted from that section (1 inserted,
1 deleted) and plus or minus the number of rows moved into or out of
that section (0 moved in, 0 moved out).'
My goal is to show "No Record found" if the table array is empty.
This is the code I'm using. When I delete the last record from table array the app crashes. How is it possible to reload the table and show "No Record Found" label?
// Customize the number of rows in the table view.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if ([idArray count]==0) {
return 3;
}
else
{
return [idArray count];
}
}
// Customize the appearance of table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSLog(#"array count %d",[idArray count]);
if ([idArray count] == 0) {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
cell.textLabel.textAlignment = UITextAlignmentCenter;
tableView.userInteractionEnabled = NO;
self.navigationItem.leftBarButtonItem.enabled = NO;
NSUInteger row = [indexPath row];
switch (row) {
case 0:
cell.textLabel.text = #"";
break;
case 1:
cell.textLabel.text = #"";
break;
case 2:
cell.textLabel.text = #"No Records Found";
break;
default:
break;
}
return cell;
}
else
{ static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
}
tableView.userInteractionEnabled = YES;
self.navigationItem.leftBarButtonItem.enabled = YES;
// Set up the cell
identify *idItems = [idArray objectAtIndex:indexPath.row];
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:#"dd MMM,yyyy"];
NSString *dateStr = [formatter stringFromDate:idItems.Date];
UIImageView *accDis = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"Arrow.png"]];
cell.accessoryView = accDis;
self.idTableView.separatorColor = [UIColor colorWithRed:150.0/255.0 green:150.0/255.0 blue:150.0/255.0 alpha:1];
cell.textLabel.textColor = [UIColor blackColor];
cell.textLabel.font = [UIFont boldSystemFontOfSize:18];
cell.textLabel.adjustsFontSizeToFitWidth = YES;
cell.detailTextLabel.textColor = [UIColor colorWithRed:100.0/255.0 green:100.0/255.0 blue:100.0/255.0 alpha:1];
cell.detailTextLabel.font = [UIFont italicSystemFontOfSize:16];
cell.detailTextLabel.adjustsFontSizeToFitWidth = YES;
NSString *detailText = [NSString stringWithFormat:#"%# - %#",dateStr,idItems.GeoCode];
if (idItems.Image == NULL) {
cell.imageView.image = [UIImage imageNamed:#"icon58x58.png"];
}
else
{
//pass image to fix size 50 X 50
//UIImage *newImage = [self postProcessImage:idItems.Image];
cell.imageView.image = idItems.thumb;//newImage;
cell.imageView.contentMode=UIViewContentModeScaleAspectFill;
}
cell.textLabel.text = idItems.TypeName;
cell.detailTextLabel.text = detailText;
return cell;
}
}
- (void)tableView:(UITableView *)tv commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath {
if(editingStyle == UITableViewCellEditingStyleDelete) {
if ([idArray count] >=1)
{
[idTableView beginUpdates];
//Get the object to delete from the array.
identifyObject = [appDelegate.idArray objectAtIndex:indexPath.row];
//Delete the object from the table.
[self.idTableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[appDelegate removeID:identifyObject];
if ([idArray count] == 0) {
[self.idTableView insertRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
[idTableView endUpdates];
}
}
}
The problem is that a tableview expects the operations performed on the view to match the data source. You have one record in the table, and you remove it. The tableview is expecting the datasource to now contain zero records, but because of your "no records found" logic, it actually returns a value of 3, hence the consistency error, and your crash.
The bug seems to be this part:
if ([idArray count] == 0) {
[self.idTableView insertRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
I assume this was intended to insert the "no records found" row into the table when the last line is deleted, but since your "no records found" actually spans three rows, you need to insert three rows here instead, like this:
if ([idArray count] == 0) {
[self.idTableView insertRowsAtIndexPaths:[NSArray arrayWithObjects:
[NSIndexPath indexPathForRow:0 inSection:indexPath.section],
[NSIndexPath indexPathForRow:1 inSection:indexPath.section],
[NSIndexPath indexPathForRow:2 inSection:indexPath.section],
nil] withRowAnimation:UITableViewRowAnimationFade];
}
For you own sanity however, can I suggest a different approach? Rather than trying to keep your table and datasource in sync whilst juggling these fake three rows of data that are only there for display purposes, why not just insert a UILabel into your view hierarchy (either in front of or behind the tableview) that says "no records found" and show/hide it based on whether the table has any data? That way you can precisely control its position and appearance without having to screw around with your datasource logic.
General rules for dealing with deleting rows are:
Deal with your model
Deal with row's animation
So for example:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
NSInteger row = [indexPath row];
[yourModel removeObjectAtIndex:row]; // you need to update your model
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
}
Now, in my opinion the correct code could be the following (I've written some comments to guide you).
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
//Get the object to delete from the array.
identifyObject = [appDelegate.idArray objectAtIndex:indexPath.row];
[appDelegate removeID:identifyObject]; // update model first
// now you can check model count and do what you want
if ([appDelegate.idArray count] == 0) // I think you mean appDelegate.idArray
{
// do what you want
// with [self.idTableView insertRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
else
{
[self.idTableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
}
}
Hope it helps.
I was using same approach where I used a cell for "No rows" warning.
For me, this worked:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
[favs removeObjectAtIndex:indexPath.section];
if ([favs count] == 0) {
[tableView reloadRowsAtIndexPaths:[[NSArray alloc]initWithObjects:indexPath, nil] withRowAnimation:UITableViewRowAnimationFade];
[tableView setEditing:NO animated:YES];
// Remove Edit bar button item
self.navigationItem.rightBarButtonItem = nil;
}
else {
// Animate the deletion from the table.
[tableView deleteRowsAtIndexPaths:[[NSArray alloc]initWithObjects:indexPath, nil] withRowAnimation:UITableViewRowAnimationFade];
}
}
}

Core Data Books Example Add Custom Cell

I have based my app upon the Core Data Books example project Apple provide and I have made each cell a custom cell which which displays certain data, works magically.
However, now I am trying to add a second custom cell. It is a singular cell which will always be the first cell in the table, then all of the fetched results from core data go below that.
I have tried like so:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row == 0)
{
static NSString *CellIdentifier = #"statsCell";
GuestStatsCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
{
cell = [[GuestStatsCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
//Configure the cell.
[self configureStatsCell:cell];
return cell;
}
else
{
static NSString *CellIdentifier = #"guestCell";
customGuestCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
{
cell = [[customGuestCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
// Configure the cell.
[self configureGuestCell:cell atIndexPath:indexPath];
return cell;
}
}
Along with some other changes to try and get things working. A few problems.
Firstly, the view loads fine, I get my custom cell at index 0 and then my core data cells load up below that. First problem, the very first core data cell isn't visible as its hidden behind my custom cell but if I tap it I do get the detail view for that core data entry. So I need to work out how I can stop it going behind that custom cell.
I have tried to remedy this, for example, it seems logical that this chunk:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
id <NSFetchedResultsSectionInfo> sectionInfo = [[fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
}
So, this is only setting enough cells for the core data records and doesn't account for that additional custom cell. So I try something as simple that this:
return [sectionInfo numberOfObjects] + 1;
Then this results in this line causing me to get a crash and index beyond bounds error:
GuestInfo *guest = [fetchedResultsController objectAtIndexPath:indexPath];
That line is from the configureCell method which setups the core data record cells. So Im pretty stumped as to what to do.
Next problem, when I edit an attribute of any core data record in the detail view, returning to the tableview my custom cell doesn't show any changes. It displays stats to do with the core data info. Only the other cells get updated.
ANy help would be much appreciated, and if you need to see anymore code or need more explanation let me know.
EDIT:
More code as requested to help me. This is the NSFetchedController code.
- (NSFetchedResultsController *)fetchedResultsController
{
if (fetchedResultsController != nil)
{
return fetchedResultsController;
}
// Create and configure a fetch request with the Book entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"GuestInfo" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
// Create the sort descriptors array.
NSSortDescriptor *lastNameDescriptor = [[NSSortDescriptor alloc] initWithKey:#"lastName" ascending:YES];
NSSortDescriptor *firstNameDescriptor = [[NSSortDescriptor alloc] initWithKey:#"firstName" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:lastNameDescriptor, firstNameDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
// Create and initialize the fetch results controller.
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:#"displayOrder" cacheName:#"Root"];
self.fetchedResultsController = aFetchedResultsController;
fetchedResultsController.delegate = self;
// Memory management.
//No releasing with ARC!
return fetchedResultsController;
}
/**
Delegate methods of NSFetchedResultsController 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:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self configureGuestCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
[self.tableView reloadData];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] 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 {
// The fetch controller has sent all current change notifications, so tell the table view to process all updates.
[self.tableView endUpdates];
}
I'm implementing the same function and using offset with indexPath #Toro's solution work just fine. Don't know if there is a better way or not here is my adjustment for those interested.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects] + 1;
}
Add one extra row for my custom cell, my fetched result have no section so I this work just fine.
- (NSIndexPath *)adjustedIndexPath:(NSIndexPath *)indexPath
{
NSIndexPath *newPath = [NSIndexPath indexPathForRow:indexPath.row - 1 inSection:indexPath.section];
return newPath;
}
private method for adjust indexpath to retrieve right object from NsFetchedResultController.
Here are method that I apply adjustedIndexPath:
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath

moveRowAtIndexPath in UITableView causes incorrect animation

I have a simple UITableView Controller that shows CoreData. I'm trying to implement - (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath; and having trouble with the animation. The Core Data store gets updated, but the animation is not working.
How can I get the animation to correctly reflect the changes that are happening to the core data objects?
For example:
Initial order:
After item 2 to the top:
or, Initial Order:
After moving item 1 to position 3:
Here's the relevant code:
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {
//this implementation is from this tutorial: http://www.cimgf.com/2010/06/05/re-ordering-nsfetchedresultscontroller/
NSMutableArray *things = [[fetchedResultsController fetchedObjects] mutableCopy];
// Grab the item we're moving.
NSManagedObject *thing = [fetchedResultsController objectAtIndexPath:fromIndexPath];
// Remove the object we're moving from the array.
[things removeObject:thing];
// Now re-insert it at the destination.
[things insertObject:thing atIndex:[toIndexPath row]];
// All of the objects are now in their correct order. Update each
// object's displayOrder field by iterating through the array.
int i = 0;
for (NSManagedObject *mo in things)
{
[mo setValue:[NSNumber numberWithInt:i++] forKey:#"order"];
}
NSLog(#"things: %#", things);
[things release], things = nil;
[managedObjectContext save:nil];
}
and the delegate:
- (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:
NSLog(#"ResultsChangeInsert:");
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
The problem was caused by the delegate interfering with my reordering implementation.
I added a bool before saving my ManagedObjectContext:
reordering = YES;
[managedObjectContext save:nil];
and used it to skip out of any of the FetchedResultsController Delegate functions. Not the best solution, but it works for this implementation. I'd be grateful for any comments / answers explaining why this happened.
/**
Delegate methods of NSFetchedResultsController to respond to additions, removals and so on.
*/
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
if (!reordering) {
// The fetch controller is about to start sending change notifications, so prepare the table view for updates.
NSLog(#"controllerWillChangeContent:");
[self.tableView beginUpdates];
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
if (!reordering) {
NSLog(#"didChangeObject:");
UITableView *tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
NSLog(#"ResultsChangeInsert:");
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
/*case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;*/
}
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
if (!reordering) {
NSLog(#"didChangeSelection:");
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 {
if (!reordering) {
NSLog(#"didChangeContent:");
// The fetch controller has sent all current change notifications, so tell the table view to process all updates.
[self.tableView endUpdates];
}else {
reordering = NO;
}
}
I had the same problem, and I fixed it by moving the saving code to a "save" place. i.e.:
- (void)setEditing:(BOOL)editing animated:(BOOL)animated
{
[super setEditing:editing animated:animated];
if (!editing) {
//save here
}
}
I think it's understandable that things would be messed up if you save in the middle of moving things around. And -(void)setEditing seems to be a good place to do the saving.
btw, thanks for pointing out the cause of the problem!

How to implement delayed/batched table view update from NSFetchedResultsControllerDelegate?

The documentation says:
You should consider carefully whether you want to update the table view as each change is made. If a large number of modifications are made simultaneously—for example, if you are reading data from a background thread— /.../ you could just implement controllerDidChangeContent: (which is sent to the delegate when all pending changes have been processed) to reload the table view.
This is exactly what I'm doing: I'm processing incoming changes in a background thread with a different ManagedObjectContext, and merge the results into the main thread MOC with mergeChangesFromContextDidSaveNotification:. So far so good.
I chose to not implement controller:didChangeObject:... and would instead like to do the batched update that the document suggests.
Question/problem: the document doesn't elaborate how to actually implement the batched update? Should I just call [tableview reloadData] in controllerDidChangeContent: or is there a less intrusive way that saves me from a full reload?
One thought I have: I could take note of mergeChangesFrom... notification that contains the changed objects, figure out their indexpaths, and just call tableview:ReloadRowsAtIndexPaths: for them. But is there any authoritative info, recommendations or examples? Or just [tableview reloadData]?
(Aside: controller:didChangeObject:... started behaving really erratically when it received a set of batched updates, even though the same updating code [that I now put in background thread] was fine before when it was running on the main thread, but of course locking up the UI.)
I would just call reloadData in controllerDidChangeContent:.
For animating individual changes to the table, Apple's boiler plate code (iOS SDK 4.3.x) looks like this:
- (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: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 believe this is what you're looking for:
Instead of calling
deleteSections
insertSections
reloadSections
deleteRowsAtIndexPaths
insertRowsAtIndexPaths
reloadRowsAtIndexPaths
as the changes are coming in at controller:didChangeObject:atIndexPath:forChangeType:newIndexPath, collect the indexPaths and sections in properties. Then execute the batch in controllerDidChangeContent:.
Ref: http://www.fruitstandsoftware.com/blog/2013/02/19/uitableview-and-nsfetchedresultscontroller-updates-done-right/
Edit
Here is a slightly different, more contrived, but also more compact, version of the approach mentioned in the link:
#property (nonatomic, strong) NSMutableArray *sectionChanges;
#property (nonatomic, strong) NSMutableArray *objectChanges;
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
}
- (void)controller:(NSFetchedResultsController *)controller
didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex
forChangeType:(NSFetchedResultsChangeType)type
{
NSMutableDictionary *sectionChange = [NSMutableDictionary new];
switch(type)
{
case NSFetchedResultsChangeInsert:
case NSFetchedResultsChangeDelete:
case NSFetchedResultsChangeUpdate:
sectionChange[#(type)] = #(sectionIndex);
break;
}
[self.sectionChanges addObject:sectionChange];
}
- (void)controller:(NSFetchedResultsController *)controller
didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
NSMutableDictionary *objectChange = [NSMutableDictionary new];
switch(type)
{
case NSFetchedResultsChangeInsert:
objectChange[#(type)] = newIndexPath;
break;
case NSFetchedResultsChangeDelete:
case NSFetchedResultsChangeUpdate:
objectChange[#(type)] = indexPath;
break;
case NSFetchedResultsChangeMove:
objectChange[#(type)] = #[indexPath, newIndexPath];
break;
}
[self.objectChanges addObject:objectChange];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
UITableView *tableView = self.tableView;
NSUInteger totalChanges = self.sectionChanges.count + self.objectChanges.count;
if (totalChanges > 0)
{
[tableView beginUpdates];
if (self.sectionChanges.count > 0)
{
for (NSDictionary *change in self.sectionChanges)
{
[change enumerateKeysAndObjectsUsingBlock:^(NSNumber *key, id obj, BOOL *stop) {
NSFetchedResultsChangeType type = [key unsignedIntegerValue];
switch (type)
{
case NSFetchedResultsChangeInsert:
[tableView insertSections:[NSIndexSet indexSetWithIndex:[obj unsignedIntegerValue]] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteSections:[NSIndexSet indexSetWithIndex:[obj unsignedIntegerValue]] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeUpdate:
[tableView reloadSections:[NSIndexSet indexSetWithIndex:[obj unsignedIntegerValue]] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
}
}];
}
}
else if (self.objectChanges > 0)
{
NSMutableArray *indexPathsForUpdatedObjects = [NSMutableArray new];
for (NSDictionary *change in self.objectChanges)
{
[change enumerateKeysAndObjectsUsingBlock:^(NSNumber *key, id obj, BOOL *stop) {
NSFetchedResultsChangeType type = [key unsignedIntegerValue];
switch (type)
{
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:#[obj] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:#[obj] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeUpdate:
[indexPathsForUpdatedObjects addObject:obj];
//[tableView reloadRowsAtIndexPaths:#[obj] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeMove:
[tableView moveRowAtIndexPath:obj[0] toIndexPath:obj[1]];
break;
}
}];
}
[tableView endUpdates];
for (NSIndexPath *indexPath in indexPathsForUpdatedObjects)
if ([tableView.indexPathsForVisibleRows containsObject:indexPath])
[self updateCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
}
[self.sectionChanges removeAllObjects];
[self.objectChanges removeAllObjects];
}
else if (totalChanges < 1)
{
[tableView reloadData];
}
}
- (NSMutableArray *)sectionChanges
{
if (!_sectionChanges)
_sectionChanges = [NSMutableArray new];
return _sectionChanges;
}
- (NSMutableArray *)objectChanges
{
if (!_objectChanges)
_objectChanges = [NSMutableArray new];
return _objectChanges;
}
Note that this solution is not perfect in that it does not update objects if there are changes to sections and those objects are in a section that is not updated. Should be easy to fix (and is irrelevant for many applications).
You could note the last index path in your datasource & then do this -
NSIndexPath *indexPath = nil;
NSMutableArray *newResults = [[NSMutableArray alloc] init];
if(gLastResultIndex < [self.dataSource count])
{
for(int i=(gLastResultIndex); i<[self.dataSource count]; i++)
{
indexPath = [NSIndexPath indexPathForRow:i inSection:0];
[newResults addObject:indexPath];
}
[self.table beginUpdates];
[self.table insertRowsAtIndexPaths:newResults withRowAnimation:UITableViewRowAnimationNone];
[self.table endUpdates];
}
[newResults release];
This is selectively adding new rows to your existing UITableView. reload table reloads all cells in your table which you might not need. I use reload table only when the entire datasource changes...