使用 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