断言失败 – 与NSFetchedResultsController

我有2个托pipe对象上下文:(1)创buildNSMainQueueConcurrencyType UI /主线程使用的NSPrivateQueueConcurrencyType ;(2)创build为networking使用的NSPrivateQueueConcurrencyType 。 这两个上下文都转到持久存储(即,我没有使用父/子上下文)。

对于视图控制器,我正在使用UITableViewControllerNSFetchedResultsController使用第一个用户界面pipe理的对象上下文。

我通过观察NSManagedObjectContextDidSaveNotification将来自第二个托pipe对象上下文的更改合并到第一个上下文中。

该应用程序正常工作,直到它处理一个networking响应,导致一个新的对象被插入, 在第二个上下文中删除一个现有的对象。 当保存第二个上下文时,触发NSManagedObjectContextDidSaveNotification并将更改合并到第一个上下文中。 调用NSFetchedResultsController的委托方法,并向表中添加新行,但表示已删除对象的行不会被删除。

如果我在表视图上尝试其他操作,比如重载表或更新其他对象,我在控制台日志中得到这个断言:

 *** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit/UIKit-2380.17/UITableView.m:1070 CoreData: error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to - controllerDidChangeContent:. Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (6) must be equal to the number of rows contained in that section before the update (7), plus or minus the number of rows inserted or deleted from that section (6 inserted, 6 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out). with userInfo (null) 

通常情况下,如果在使用UITableView的批量更新方法时忘记更新模型对象,则会出现此错误,但在这种情况下, NSFetchedResultsController正在完成所有工作。 我的委托方法是样板:

 - (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 { NSLog(@" didChangeObject type=%d indexPath=%@ newIndexPath=%@", type, indexPath, newIndexPath); UITableView *tableView = self.tableView; switch(type) { case NSFetchedResultsChangeInsert: [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade]; break; case NSFetchedResultsChangeDelete: [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade]; break; case NSFetchedResultsChangeUpdate: [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; break; case NSFetchedResultsChangeMove: [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade]; [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade]; break; } } - (void)controllerDidChangeContent:(NSFetchedResultsController *)controller { [self.tableView endUpdates]; } 

我的UITableViewDataSource tableView:cellForRowAtIndexPath方法是:

 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier]; cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; } [self configureCell:cell atIndexPath:indexPath]; return cell; } 

configureCell:是:

 - (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath { Event *event = (Event *)[self.fetchedResultsController objectAtIndexPath:indexPath]; cell.textLabel.text = [[event valueForKey:@"timeStamp"] description]; NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:@"Owner"]; NSArray *objects = [self.managedObjectContext executeFetchRequest:fetchRequest error:nil]; cell.detailTextLabel.text = [objects.lastObject name]; // simplified } 

NSFetchRequest如果您在准备您的单元格响应tableView:cellForRowAtIndexPath:时使用NSFetchedResultsController非常困惑。 如果你不执行一个NSFetchRequest ,一切都很好。 但是,如果你这样做,它会触发NSFetchedResultsController执行进一步的更改通知,这对UITableView不利。

解决方法是在NSFetchRequest上设置includesPendingChanges = NO

我已经打开了关于这个问题的雷达问题 – 问题ID 14048101 – 一个详细的例子和示例应用程序。 此错误在iOS 5.1,6.0和6.1上重现。

在我的示例应用程序中,我将日志logging添加到Xcode的CoreData模板以loggingNSFetchedResultsController委托方法的input/离开。 当我在networking上下文中插入+删除对象时,logging显示:

01:=>(before)mergeChangesFromContextDidSaveNotification 02:=>(enter)controllerWillChangeContent count = 4 03:<=(离开)controllerWillChangeContent count = 4 04:didChangeObject type = 1 indexPath =(null)newIndexPath = 2 indexes [0,0] 05:=>(进入)controllerDidChangeContent count = 5

在这一点上,一切都很好。 controllerDidChangeContent:已被调用来处理1插入,它调用调用tableView:cellForRowAtIndexPath: [tableView endUpdates] ,它调用configureCell:atIndexPath:

06:=>(回车)在第0行configuration单元格

在这一点上, configureCell:atIndexPath:创build一个NSFetchRequest并调用[self.managedObjectContext executeFetchRequest:error:] – 这里开始[self.managedObjectContext executeFetchRequest:error:] 。 在插入处理完成之前(我们inputcontrollerDidChangeContent:在#05行,不要离开,直到行#16),执行这个获取请求将触发上下文中剩余更改(1个删除和3个更新)的处理。

07:=>(进入)controllerWillChangeContent count = 5 08:<=(离开)controllerWillChangeContent count = 5 09:didChangeObject type = 2 indexPath = 2 indexes [0,4] newIndexPath =(null)10:didChangeObject type = 4 indexPath = 2 indexson [0,2] newIndexPath =(null)11:didChangeObject type = 4 indexPath = 2 index [0,1] newIndexPath =(null)12:didChangeObject type = 4 indexPath = 2 index [0,3] newIndexPath =( null)13:=>(进入)controllerDidChangeContent count = 4

此时,框架正在对controllerDidChangeContent:进行重新调用。

14:<=(离开)controllerDidChangeContent count = 4 15:<=(离开)在第0行configuration单元格16:<=(离开)controllerDidChangeContent count = 4 17:<=(after)mergeChangesFromContextDidSaveNotification

此时,您可以在界面中看到:(1)添加了新的单元格,(2)更新了3个单元格,(3)删除的单元格仍然可见,这是错误的。

在用户界面进一步的行动后,我通常会得到一个Assertion failure或消息发送到一个无效的对象exception。

我的示例应用程序可在https://github.com/peymano/CoreDataFetchedResultsController