在 - [UITableView _endCellAnimationsWithContext:]中的断言失败与NSFetchedResultsController [英] Assertion failure in -[UITableView _endCellAnimationsWithContext:] with NSFetchedResultsController

查看:140
本文介绍了在 - [UITableView _endCellAnimationsWithContext:]中的断言失败与NSFetchedResultsController的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有2个管理对象上下文:(1)创建为 NSMainQueueConcurrencyType ,由UI /主线程使用,(2)创建为 NSPrivateQueueConcurrencyType ,由网络使用。这两个上下文都去持久存储(即,我不使用父/子上下文)。



对于视图控制器,我使用 UITableViewController 与使用第一个UI管理对象上下文的 NSFetchedResultsController



我通过观察 NSManagedObjectContextDidSaveNotification ,将第二个受管对象上下文中的更改合并到第一个上下文中。

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

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

  ***  -   -  [UITableView _endCellAnimationsWithContext:]中的声明失败,
/SourceCache/UIKit/UIKit-2380.17/UITableView.m:1070

CoreData:error:严重的应用程序错误。在对_
controllerDidChangeContent:的调用期间,从NSFetchedResultsController的委托捕获到
异常。无效的更新:
节0中的行数无效。
更新(6)后现有节中包含的行数必须等于该节中包含的行数
在更新(7)之前,加或减从该部分插入或删除的行数
(插入6,删除6),并且加上或减去移入或移出该部分的行数
(0移入,0移出)。
userInfo(null)

通常情况下,如果您忘记了使用 UITableView 的批量更新方法更新您的模型对象,但在这种情况下, NSFetchedResultsController 正在做所有的工作。我的委托方法是样板:

   - (void)controllerWillChangeContent:(NSFetchedResultsController *)controller 
{
[self.tableView beginUpdates]; (NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type(NSFetchedResultsChangeType)(NSFetchedResultsChangeType)
{
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 = [object.lastObject name]; // simplified
}


解决方案

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

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



我打开了一个关于这个问题的雷达问题 - 问题ID 14048101 - 一个详细的示例和示例应用程序。



在我的示例应用程序中,我添加了日志记录到Xcode的CoreData模板,以登录 NSFetchedResultsController 委托方法。当我在网络上下文中插入+删除对象时,日志显示:



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



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



06 :=>(enter)在第0行配置单元



此时, configureCell:atIndexPath: code> NSFetchRequest
并调用 [self.managedObjectContext executeFetchRequest:error:] - 这里开始了坏处。执行此获取请求触发在处理插入操作完成之前处理上下文中的剩余更改(1删除和3更新)(我们在线#05上输入 controllerDidChangeContent:并且不要离开直到第16行)。



07:=>(enter)controllerWillChangeContent count = 5
08:< =(leave)controllerWillChangeContent count = 5
09:didChangeObject type = 2 indexPath = 2 indexes [0,4] newIndexPath =(null)
10:didChangeObject type = 4 indexPath = 2 indexes [0,2] newIndexPath = )
11:didChangeObject type = 4 indexPath = 2 indexes [0,1] newIndexPath =(null)
12:didChangeObject type = 4 indexPath = 2 indexes [0,3] newIndexPath = b $ b 13:=>(enter)controllerDidChangeContent count = 4



此时,框架重新调用 controllerDidChangeContent :



14:< =(leave)controllerDidChangeContent count = 4
15:< =在第0行
16:< =(leave)controllerDidChangeContent count = 4
17:< =(之后)mergeChangesFromContextDidSaveNotification



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



在UI中进一步操作后,我通常会得到 Assertion失败或者发送到无效对象异常的消息。



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


I have 2 managed object contexts: (1) created as NSMainQueueConcurrencyType that is used by the UI/main thread and (2) created as NSPrivateQueueConcurrencyType that is used by the networking. Both of these contexts go to the persistent store (i.e., I'm not using parent/child contexts).

For the view controller, I'm using a UITableViewController with a NSFetchedResultsController that uses the 1st UI managed object context.

I am merging changes from the 2nd managed object context into the 1st context by observing the NSManagedObjectContextDidSaveNotification.

The app works fine until it processes a network response that causes a new object to be inserted and an existing object to be deleted in the 2nd context. When the 2nd context is saved, the NSManagedObjectContextDidSaveNotification fires and the changes are merged into the 1st context. The delegate methods of NSFetchedResultsController are invoked and a new row is added to the table, but the row representing the deleted object is *not removed.

If I attempt other actions on the table view, like reloading the table or updating other objects, I get this assert in the console log:

*** 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)

Typically, you get this error if you've forgotten to update your model objects when using UITableView's batch update methods, but in this case, NSFetchedResultsController is doing all work. My delegate methods are boilerplate:

- (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];
}

My UITableViewDataSource tableView:cellForRowAtIndexPath method is:

- (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;
}

And configureCell: is:

- (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
}

解决方案

NSFetchedResultsController gets terribly confused if you use an NSFetchRequest when preparing your cell in response to tableView:cellForRowAtIndexPath:. If you don't execute a NSFetchRequest, all is well. However, if you do, it triggers NSFetchedResultsController to perform further change notifications, which does bad things to UITableView.

The workaround for this is to set includesPendingChanges = NO on your NSFetchRequest.

I have opened a radar issue about this -- problem id 14048101 -- with a detailed example and sample app. This bug reproduces on iOS 5.1, 6.0, and 6.1.

In my sample app, I added logging to Xcode's CoreData template to log enter/leave of NSFetchedResultsController delegate methods. When I insert + delete objects on the network context, the logging shows:

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

At this point, all is good. controllerDidChangeContent: has been called to process the 1 insert, which calls [tableView endUpdates], which calls tableView:cellForRowAtIndexPath:, which calls configureCell:atIndexPath:.

06: => (enter) configure cell at row 0

At this point, configureCell:atIndexPath: creates an NSFetchRequest and calls [self.managedObjectContext executeFetchRequest:error:] -- here begins the badness. Executing this fetch request triggers the processing of the remaining changes in the context (1 delete and 3 updates) before processing of the insert has finished (we entered controllerDidChangeContent: on line #05 and don't leave until line #16).

07: => (enter) controllerWillChangeContent count=5 08: <= (leave) controllerWillChangeContent count=5 09: didChangeObject type=2 indexPath= 2 indexes [0, 4] newIndexPath=(null) 10: didChangeObject type=4 indexPath= 2 indexes [0, 2] newIndexPath=(null) 11: didChangeObject type=4 indexPath= 2 indexes [0, 1] newIndexPath=(null) 12: didChangeObject type=4 indexPath= 2 indexes [0, 3] newIndexPath=(null) 13: => (enter) controllerDidChangeContent count=4

At this point, the framework is making a re-entrant call tocontrollerDidChangeContent:.

14: <= (leave) controllerDidChangeContent count=4 15: <= (leave) configure cell at row 0 16: <= (leave) controllerDidChangeContent count=4 17: <= (after) mergeChangesFromContextDidSaveNotification

At this point, you can see in the UI that: (1) a new cell has been added, (2) 3 cells were updated, and (3) the deleted cell is still visible, which is wrong.

After further action in the UI, I typically get an Assertion failure or message sent to an invalid object exception.

My sample app is available at https://github.com/peymano/CoreDataFetchedResultsController

这篇关于在 - [UITableView _endCellAnimationsWithContext:]中的断言失败与NSFetchedResultsController的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆