I have a tableview in my app that displays the entries in an underlying COreData entity. Each row in the entity is a unique-ID of a message sender and their message count.
When view first loads, the Tableview correctly displays all the entries in the CoreData entity that is linked to the NSFetchedResultsController tied to the Table View. But when the view is being shown and a new element is added to the CoreData entity or modified (due to an incoming message), the table view gets completely messed up. It starts to show the same element of the table twice. It never expands the number of rows even when new element is inserted etc etc.
When I do a NSLog of the elements in the underlying CoreData entity, I see that they are updated correctly and there is no duplication. So, it is not clear why the NSFetchedResultsController is not displaying the right thing.
The delegate code is all boiler plate from Apple documentation. The view controller code is below. Any pointers will be greatly appreciated.
@implementation TweetListTableViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Uncomment the following line to preserve selection between presentations.
// self.clearsSelectionOnViewWillAppear = NO;
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem;
self.title = @"Tweets";
AppDelegate *app = (AppDelegate*)[[UIApplication sharedApplication] delegate];
self.managedObjectContext = app.managedObjectContext;
[self initializeFetchedResultsController] ;
}
- (void)viewDidUnload {
self.fetchedResultsController = nil;
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.tableView performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:NO];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#pragma mark - NSFetchedResultsController helper methods
- (void)initializeFetchedResultsController
{
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"UniqueUserTable"];
NSSortDescriptor *timeSort = [NSSortDescriptor sortDescriptorWithKey:@"mostRecentSentTime" ascending:NO] ;
NSSortDescriptor *uidSort = [NSSortDescriptor sortDescriptorWithKey:@"sendingUserID" ascending:YES] ;
[request setSortDescriptors:[NSArray arrayWithObjects:timeSort, uidSort, nil]];
[self setFetchedResultsController:[[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:nil]];
[[self fetchedResultsController] setDelegate:self];
NSError *error = nil;
if (![[self fetchedResultsController] performFetch:&error]) {
NSLog(@"Failed to initialize FetchedResultsController: %@\n%@", [error localizedDescription], [error userInfo]);
abort();
}
}
#pragma mark - Table view data source
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath*)indexPath
{
UniqueUserTable *uutableEntry = [[self fetchedResultsController] objectAtIndexPath:indexPath];
// Populate cell from the NSManagedObject instance
cell.textLabel.text=uutableEntry.sendingUserID ;
cell.detailTextLabel.text=[NSString stringWithFormat:@"%@ tweets",uutableEntry.numberOfMessagesSent] ;
DDLogVerbose(@"Row %ld being updated with values text = %@, detailText=%@",(long)indexPath.row,cell.textLabel.text,cell.detailTextLabel.text) ;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"TweetCelldentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.textLabel.font = [UIFont systemFontOfSize:18.0];
if (!cell)
{
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
DDLogVerbose(@"Initializing cell") ;
}
// Set up the cell
//To get section use indexPath.section
//To get row use indexPath.row
[self configureCell:cell atIndexPath:indexPath];
return cell;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return [[[self fetchedResultsController] sections] count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
id< NSFetchedResultsSectionInfo> sectionInfo = [[self fetchedResultsController] sections][section];
DDLogVerbose(@"Number of rows to show in table = %ld",(unsigned long)[sectionInfo numberOfObjects]) ;
return ([sectionInfo numberOfObjects]);
}
#pragma mark - NSFetchedResultsControllerDelegate
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
[[self tableView] beginUpdates];
DDLogVerbose(@"In controllerWillChangeContent") ;
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
DDLogVerbose(@"In controller didChangeSection for change type %d",type) ;
switch(type) {
case NSFetchedResultsChangeInsert:
[[self tableView] insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[[self tableView] deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeMove:
case NSFetchedResultsChangeUpdate:
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
switch(type) {
case NSFetchedResultsChangeInsert:
[[self tableView] insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
DDLogVerbose(@"In controller didChangeObject for change type NSFetchedResultsChangeInsert with row index = %ld",(long)newIndexPath.row) ;
break;
case NSFetchedResultsChangeDelete:
[[self tableView] deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
DDLogVerbose(@"In controller didChangeObject because row %ld was deleted",(long)indexPath.row) ;
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:[[self tableView] cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
DDLogVerbose(@"In controller didChangeObject because row %ld was updated",(long)indexPath.row) ;
break;
case NSFetchedResultsChangeMove:
[[self tableView] deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
[[self tableView] insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
DDLogVerbose(@"In controller didChangeObject because row %ld was moved to %ld",(long)indexPath.row,(long)newIndexPath.row) ;
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[[self tableView] endUpdates];
[[self tableView] beginUpdates];
DDLogVerbose(@"In controllerDidChangeContent") ;
}
#pragma mark - Handling compose button
- (void)actionCompose:(UIBarButtonItem *)sender
//-------------------------------------------------------------------------------------------------------------------------------------------------
{
MessageDisplayViewController *vc = [MessageDisplayViewController messagesViewController];
vc.hidesBottomBarWhenPushed = YES;
vc.title = @"New Tweet";
vc.recipient = @"*" ;
[self.navigationController pushViewController:vc animated:YES];
}
@end
Try Removing this line : //[[self tableView] beginUpdates];
The beginUpdates must always be followed by a call to the endUpdates method.
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[[self tableView] endUpdates];
//[[self tableView] beginUpdates]; <====== Remove this
DDLogVerbose(@"In controllerDidChangeContent") ;
}
As per the documentation :
Call this method if you want subsequent insertions, deletion, and selection operations (for example, cellForRowAtIndexPath: and indexPathsForVisibleRows) to be animated simultaneously. You can also use this method followed by the endUpdates method to animate the change in the row heights without reloading the cell. This group of methods must conclude with an invocation of endUpdates. These method pairs can be nested. If you do not make the insertion, deletion, and selection calls inside this block, table attributes such as row count might become invalid. You should not call reloadData within the group; if you call this method within the group, you must perform any animations yourself.