使用 NSFetchedResultsController 在 TableView 上显示的元素列表与底层 CoreData 实体不匹配

Posted

技术标签:

【中文标题】使用 NSFetchedResultsController 在 TableView 上显示的元素列表与底层 CoreData 实体不匹配【英文标题】:The list of elements shown on TableView with NSFetchedResultsController does not match underlying CoreData entity 【发布时间】:2015-11-16 17:55:36 【问题描述】:

我的应用中有一个表格视图,它显示底层 COreData 实体中的条目。实体中的每一行都是消息发送者的唯一 ID 及其消息计数。

当视图首次加载时,Tableview 会正确显示 CoreData 实体中的所有条目,该实体链接到与 Table View 绑定的 NSFetchedResultsController。但是当视图被显示并且新元素被添加到 CoreData 实体或被修改(由于传入消息)时,表格视图会完全混乱。它开始两次显示表格的相同元素。即使插入新元素等,它也永远不会扩展行数。

当我对底层 CoreData 实体中的元素执行 NSLog 时,我看到它们已正确更新并且没有重复。所以,不清楚为什么 NSFetchedResultsController 没有显示正确的东西。

委托代码都是来自 Apple 文档的样板代码。视图控制器代码如下。任何指针将不胜感激。

@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

【问题讨论】:

你为什么在controllerDidChangeContent中调用[[self tableView] beginUpdates] 谢谢,解决了。您还可以在下面对 TheAppMentor 的回答评论我的问题吗? 【参考方案1】:

尝试删除此行: //[[self tableView] beginUpdates];

beginUpdates 之后必须始终调用 endUpdates 方法。

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller

    [[self tableView] endUpdates];
    //[[self tableView] beginUpdates]; <====== Remove this
    DDLogVerbose(@"In controllerDidChangeContent") ;

根据文档:

如果您希望后续的插入、删除和选择操作(例如 cellForRowAtIndexPath: 和 indexPathsForVisibleRows)同时进行动画处理,请调用此方法。您还可以使用此方法后跟 endUpdates 方法来动画行高的变化,而无需重新加载单元格。 这组方法必须以调用 endUpdates 结束。这些方法对可以嵌套。如果你不在这个块内进行插入、删除和选择调用,行计数等表属性可能会失效。你不应该在组内调用reloadData;如果您在组内调用此方法,则必须自己执行任何动画。

【讨论】:

TheAppMentor,一个简单的问题。 NSFetchedResultsController 会自动处理下面的场景吗?应用程序获取更新实体中现有条目的传入消息。由于时间戳较新,排序后的 COreData 实体在 #1 位置具有此条目。因此,视图将被更新,现有条目 N 将移动到条目 1,其他条目向下移动。现在,在 NSFetchedResultsController Delegate 发起的“视图更新”完成之前,第二条消息到达并被添加到核心数据实体中。 (问题在下面继续) 这条新消息成为已排序实体的条目#1。此时会发生什么?是否会有两个单独的更新一个接一个地发生(即最终视图保证在条目 1 处看到新消息,然后旧条目 N 成为条目 2)或者除非我采取一些保护措施,否则这种竞争条件是否有可能会搞砸代码? @SmartHome,据我了解,位于 beginUpdates 和 endUpdates 之间的代码被视为单个事务。即表视图将在第一次更新时更新,然后第二次更新将在第一次更新完成后排队等待执行。这是一个难以复制和验证的场景,但我认为更新将是连续的。

以上是关于使用 NSFetchedResultsController 在 TableView 上显示的元素列表与底层 CoreData 实体不匹配的主要内容,如果未能解决你的问题,请参考以下文章

核心数据保存竞争条件错误

在 Swift 3 中难以配置 NSFetchedResultsController

为啥 beginUpdates/endUpdates 会重置表视图位置以及如何阻止它这样做?

测试使用

第一篇 用于测试使用

在使用加载数据流步骤的猪中,使用(使用 PigStorage)和不使用它有啥区别?