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



错误:

CoreData: error:严重的应用错误。在调用-controllerDidChangeContent时,从NSFetchedResultsController的委托捕获了一个异常。尝试用userInfo (null)

删除和重新加载相同的索引路径({length = 2, path = 0 - 0})

这是我典型的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()
}

in 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(仍处于测试阶段)的一个bug, Apple开发者论坛也对此进行了讨论

  • iOS 9 CoreData NSFetchedResultsController update导致UICollectionView/UITableView中的空白行

我可以从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 8上基于iOS 9.0或iOS 9.1(测试版)SDK构建时发生。

我今天在玩了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 only

在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;
        // ...
    }
}

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

NSFetchedResultsChangeType enum有四个可能的值(注释的值是mine):

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

. .无效调用将导致Insert,您将得到如下错误:

无效的更新:无效的行数在第0节。的数量更新(7)后的现有节中包含的行必须是对象之前的该节中包含的行数Update(7),加上或减去插入或删除的行数(1插入,0删除)

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

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块-请不要原样使用它们。

Credits:这最初是由iCN7发现的,来源:Apple Developer Forums - iOS 9 CoreData NSFetchedResultsController update导致UICollectionView/UITableView

出于某种原因NSFetchedResultsController调用.Update,然后.MovecontrollerWillChangeContent:被调用后。

只是看起来像这样:开始更新 -> 更新 -> 移动 -> 结束更新

只在ios8 .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
        }
    }

其他答案对我来说很接近,但我收到的是"<无效> (0x0)"作为NSFetchedResultsChangeType。我注意到它被解释为"插入"更改。因此,以下修复对我有效:

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...)
}

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

问题的发生是因为重新加载和删除了相同的indexPath(这是apple产生的一个错误),所以我改变了处理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对象的更新将产生一个删除和一个插入消息。为了正确更新单元格内容,当indexPath等于newIndexPath (section和row都相等)时,我通过
重新加载单元格。[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

相关内容

最新更新