iOS 9 - "attempt to delete and reload the same index path"

iOS 9 - "attempt to delete and reload the same index path"

这是一个错误:

CoreData: error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. attempt to delete and reload the same index path ( {length = 2, path = 0 - 0}) with userInfo (null)

这是我的典型NSFetchedResultsControllerDelegate:

func controllerWillChangeContent(controller: NSFetchedResultsController) {
    tableView.beginUpdates()
}

func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {

    let indexSet = NSIndexSet(index: sectionIndex)

    switch type {
    case .Insert:
        tableView.insertSections(indexSet, withRowAnimation: .Fade)
    case .Delete:
        tableView.deleteSections(indexSet, withRowAnimation: .Fade)
    case .Update:
        fallthrough
    case .Move:
        tableView.reloadSections(indexSet, withRowAnimation: .Fade)
    }
}

func controller(controller: NSFetchedResultsController, didChangeObject anObject: NSManagedObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {

    switch type {
    case .Insert:
        if let newIndexPath = newIndexPath {
            tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Fade)
        }
    case .Delete:
        if let indexPath = indexPath {
            tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
        }
    case .Update:
        if let indexPath = indexPath {
            tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .None)
        }
    case .Move:
        if let indexPath = indexPath {
            if let newIndexPath = newIndexPath {
                tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
                tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Fade)
            }
        }
    }
}

func controllerDidChangeContent(controller: NSFetchedResultsController) {
    tableView.endUpdates()
}

viewDidLoad() 中:

private func setupOnceFetchedResultsController() {

    if fetchedResultsController == nil {
        let context = NSManagedObjectContext.MR_defaultContext()
        let fetchReguest = NSFetchRequest(entityName: "DBOrder")
        let dateDescriptor = NSSortDescriptor(key: "date", ascending: false)

        fetchReguest.predicate = NSPredicate(format: "user.identifier = %@", DBAppSettings.currentUser!.identifier )
        fetchReguest.sortDescriptors = [dateDescriptor]
        fetchReguest.fetchLimit = 10
        fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchReguest, managedObjectContext: context, sectionNameKeyPath: "identifier", cacheName: nil)
        fetchedResultsController.delegate = self

        try! fetchedResultsController.performFetch()
    }
}

这似乎是 iOS 9(仍处于测试阶段)中的错误,在 Apple 开发者论坛中也有讨论

我可以通过 Xcode 7 beta 3 中的 iOS 9 模拟器确认问题。 我观察到对于更新的托管对象,didChangeObject: 委托方法被调用 两次: 一次是 NSFetchedResultsChangeUpdate 事件,然后是 NSFetchedResultsChangeMove事件(和 indexPath == newIndexPath)。

indexPath != newIndexPath 添加显式检查 正如上面线程中的建议似乎解决了问题:

        case .Move:
            if indexPath != newIndexPath {
                tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
                tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade)
        }

更新: 所描述的问题仅在针对 iOS 9.0 或 iOS 9.1(测试版)SDK 构建时出现在 iOS 8 上。

我今天在玩 Xcode 7 beta 6(iOS 9.0 beta 5)后想出了一些可怕的解决方法,它似乎有效。

您不能使用reloadRowsAtIndexPaths,因为在某些情况下它被调用得太早并且可能导致不一致,相反您应该手动更新您的单元格。

我仍然认为最好的选择是直接调用 reloadData

我相信你可以毫不费力地将我的代码改编为 swift,我这里有 objective-c 项目。

@property NSMutableIndexSet *deletedSections, *insertedSections;

// ...

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView beginUpdates];

    self.deletedSections = [[NSMutableIndexSet alloc] init];
    self.insertedSections = [[NSMutableIndexSet alloc] init];
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView endUpdates];
}

- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id<NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
    NSIndexSet *indexSet = [NSIndexSet indexSetWithIndex:sectionIndex];

    switch(type) {
        case NSFetchedResultsChangeDelete:
            [self.tableView deleteSections:indexSet withRowAnimation:UITableViewRowAnimationAutomatic];
            [self.deletedSections addIndexes:indexSet];
            break;

        case NSFetchedResultsChangeInsert:
            [self.tableView insertSections:indexSet withRowAnimation:UITableViewRowAnimationAutomatic];
            [self.insertedSections addIndexes:indexSet];
            break;

        default:
            break;
    }
}

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
    switch(type) {
        case NSFetchedResultsChangeDelete:
            [self.tableView deleteRowsAtIndexPaths:@[ indexPath ] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;

        case NSFetchedResultsChangeInsert:
            [self.tableView insertRowsAtIndexPaths:@[ newIndexPath ] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;

        case NSFetchedResultsChangeMove:
            // iOS 9.0b5 sends the same index path twice instead of delete
            if(![indexPath isEqual:newIndexPath]) {
                [self.tableView deleteRowsAtIndexPaths:@[ indexPath ] withRowAnimation:UITableViewRowAnimationAutomatic];
                [self.tableView insertRowsAtIndexPaths:@[ newIndexPath ] withRowAnimation:UITableViewRowAnimationAutomatic];
            }
            else if([self.insertedSections containsIndex:indexPath.section]) {
                // iOS 9.0b5 bug: Moving first item from section 0 (which becomes section 1 later) to section 0
                // Really the only way is to delete and insert the same index path...
                [self.tableView deleteRowsAtIndexPaths:@[ indexPath ] withRowAnimation:UITableViewRowAnimationAutomatic];
                [self.tableView insertRowsAtIndexPaths:@[ indexPath ] withRowAnimation:UITableViewRowAnimationAutomatic];
            }
            else if([self.deletedSections containsIndex:indexPath.section]) {
                // iOS 9.0b5 bug: same index path reported after section was removed
                // we can ignore item deletion here because the whole section was removed anyway
                [self.tableView insertRowsAtIndexPaths:@[ indexPath ] withRowAnimation:UITableViewRowAnimationAutomatic];
            }

            break;

        case NSFetchedResultsChangeUpdate:
            // On iOS 9.0b5 NSFetchedResultsController may not even contain such indexPath anymore
            // when removing last item from section.
            if(![self.deletedSections containsIndex:indexPath.section] && ![self.insertedSections containsIndex:indexPath.section]) {
                // iOS 9.0b5 sends update before delete therefore we cannot use reload
                // this will never work correctly but at least no crash. 
                UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
                [self _configureCell:cell forRowAtIndexPath:indexPath];
            }

            break;
    }
}

Xcode 7 / iOS 仅 9.0

在 Xcode 7 / iOS 9.0 NSFetchedResultsChangeMove 中仍在发送,而不是 "update"。

作为一种简单的解决方法,只需针对这种情况禁用动画:

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
    UITableViewRowAnimation animation = UITableViewRowAnimationAutomatic;

    switch(type) {

        case NSFetchedResultsChangeMove:
            // @MARK: iOS 9.0 bug. Move sent instead of update. indexPath = newIndexPath.
            if([indexPath isEqual:newIndexPath]) {
                animation = UITableViewRowAnimationNone;
            }

            [self.tableView deleteRowsAtIndexPaths:@[ indexPath ] withRowAnimation:animation];
            [self.tableView insertRowsAtIndexPaths:@[ newIndexPath ] withRowAnimation:animation];

            break;

        // ...
    }
}

其他答案对我来说很接近,但我收到“<无效> (0x0)”作为 NSFetchedResultsChangeType。我注意到它被解释为 "insert" 更改。所以以下修复对我有用:

func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case .Insert:
  // iOS 9 / Swift 2.0 BUG with running 8.4
  if indexPath == nil {
    self.tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
  }
  (etc...)
}

因为每个 "insert" 只返回一个 newIndexPath 而没有 indexPath(这个奇怪的额外插入委托调用返回时为 newIndexPath 和 indexPath 列出了相同的路径),这只是检查它是正确的类型 "insert" 并跳过其他类型。

这个问题是因为reload和delete了相同的indexPath(苹果出的bug),所以我改变了处理NSFetchedResultsChangeUpdate消息的方式。

而不是:

 [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];

我手动更新了单元格的内容:

MyChatCell *cell = (MyChatCell *)[self.tableView cellForRowAtIndexPath:indexPath];
CoreDataObject *cdo = [[self fetchedResultsController] objectAtIndexPath:indexPath];
// update the cell with the content: cdo
[cell updateContent:cdo];

事实证明效果很好。

顺便说一句: CoreData 对象的更新将产生删除和插入 message.To 正确更新单元格内容,当 indexPath 等于 newIndexPath(部分和行都相等)时,我按
重新加载单元格 [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];

示例代码如下:

- (void)controller:(NSFetchedResultsController *)controller
   didChangeObject:(id)anObject
       atIndexPath:(NSIndexPath *)indexPath
     forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath
{
    if (![self isViewLoaded]) return;
    switch(type)
    {
        case NSFetchedResultsChangeInsert:
            [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
                              withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeDelete:
            [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
                              withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeUpdate:{
            MyChatCell *cell = (MyChatCell *)[self.tableView cellForRowAtIndexPath:indexPath];
            CoreDataObject *cdo = [[self fetchedResultsController] objectAtIndexPath:indexPath];
            // update the cell with the content: cdo
            [cell updateContent:cdo];
        }
            break;

        case NSFetchedResultsChangeMove:
            if (indexPath.row!=newIndexPath.row || indexPath.section!=newIndexPath.section){
                [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
                               withRowAnimation:UITableViewRowAnimationFade];
                [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
                               withRowAnimation:UITableViewRowAnimationFade];
            }else{
                [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
            }

    }
}

我把上面的示例代码放在要点上: https://gist.github.com/dreamolight/157266c615d4a226e772

关于在 iOS8 上发生的这种情况,构建是针对 iOS9 编译的,在某些人解决的 indexPath==newIndexPath 问题之上其他答案,发生了一些非常奇怪的事情

NSFetchedResultsChangeType 枚举有四个可能的值(带有值的评论是我的):

public enum NSFetchedResultsChangeType : UInt {
    case Insert // 1
    case Delete // 2
    case Move   // 3
    case Update // 4
}

.. 但是,controller:didChangeObject:atIndexPath:forChangeType 函数有时会使用无效值 0x0.

调用

Swift 似乎默认为第一个 switch 情况,因此如果您具有以下结构:

func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
        switch type {
            case .Insert: tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
            case .Delete: tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
            case .Update: tableView.reloadRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.None)
            case .Move: tableView.moveRowAtIndexPath(ip, toIndexPath: nip)
        }
    }

.. 无效调用将导致插入,您将收到如下错误:

Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (7) 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 (1 inserted, 0 deleted)

简单地交换案例,使第一个案例是一个相当无害的更新修复了问题:

func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
        switch type {
            case .Update: tableView.reloadRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.None)
            case .Insert: tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
            case .Delete: tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
            case .Move: tableView.moveRowAtIndexPath(ip, toIndexPath: nip)
        }
    }

另一种选择是检查 type.rawValue 是否存在无效值。

注意:虽然这解决了与 OP 发布的错误消息略有不同的错误消息,但问题是相关的;很有可能一旦您解决了 indexPath==newIndexPath 问题,这个问题就会弹出。 此外,简化了上述代码块以说明顺序;例如,缺少适当的 guard 块 - 请不要按原样使用它们。

出处:这个最初是iCN7发现的,来源:Apple Developer Forums — iOS 9 CoreData NSFetchedResultsController update causes blank rows in UICollectionView/UITableView

出于某种原因 NSFetchedResultsController 调用 .Update,然后在调用 controllerWillChangeContent: 之后调用 .Move

看起来像这样:BEGIN UPDATES -> UPDATE -> MOVE - > 结束更新

Happens only under iOS 8.x

在一个更新会话期间,相同的单元格被重新加载和删除导致崩溃。

有史以来最简单的修复:

以下部分代码:

case .Update:
    if let indexPath = indexPath {
        tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
    }

替换为:

case .Update:
    if let indexPath = indexPath {

        // 1. get your cell
        // 2. get object related to your cell from fetched results controller
        // 3. update your cell using that object

        //EXAMPLE:
        if let cell = tableView.cellForRowAtIndexPath(indexPath) as? WLTableViewCell { //1
            let wishlist = fetchedResultsController.objectAtIndexPath(indexPath) as! WLWishlist //2
            cell.configureCellWithWishlist(wishlist) //3
        }
    }

确实有效