【问题标题】:iOS 9 - "attempt to delete and reload the same index path"iOS 9 - “尝试删除并重新加载相同的索引路径”
【发布时间】:2015-10-01 17:46:24
【问题描述】:

这是一个错误:

CoreData:错误:严重的应用程序错误。在调用 -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()
}

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 swift ios9 nsfetchedresultscontroller


    【解决方案1】:

    这似乎是 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)
            }
    

    【讨论】:

    • 它解决了我的问题。但是当我添加一些行时还有另一个问题:attempt to delete row 0 from section 12, but there are only 10 sections before the update with userInfo (null)。你认为这也是一个错误吗?
    • @BartłomiejSemańczyk:这可能是代码中的错误或错误。这个问题(还)没有发生在我身上,所以我不知道。你检查过 iOS 8 模拟器吗?
    • 不,它以前可以工作,在 iOS 8.3 和 Xcode7Beta3 中它不起作用。此外,我不会尝试删除任何内容,只是添加一些信息...
    • 你真的可以简单地用“indexPath != newIndexPath”来比较 2 个 NSIndexPath 实例的相等性吗?您不需要比较行和部分吗?或者使用 NSIndexPath.compare:?或者也许 isEqual?
    • XCode 7 GM build 上仍然发生!
    【解决方案2】:

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

    看起来就像这样:BEGIN UPDATES -> UPDATE -> MOVE -> END UPDATES

    仅在 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
            }
        }
    

    确实有效

    【讨论】:

    • 我想我读到 reloadRowsAtIndexPaths 实际上通常是更好的解决方案 - 在 Xcode 9 中仍然需要这种替换吗?或者我们可以在这里安全地使用 reloadRowsAtIndexPaths 吗?
    • 这里是我指的文章:oleb.net/blog/2013/02/…
    【解决方案3】:

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

    NSFetchedResultsChangeType 枚举有四个可能的值(具有值的 cmets 是我的):

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

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

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

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

    无效更新:第 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 块 - 请不要按原样使用它们。

    致谢:最初由 iCN7 发现,来源:Apple Developer Forums — iOS 9 CoreData NSFetchedResultsController update causes blank rows in UICollectionView/UITableView

    【讨论】:

    • 我刚刚实施了indexPath==newIndexPath 修复。因为我在这里使用的是 Objective-C,所以我没有遇到这个问题作为 问题,但是我仍然看到一个 NSFetchedResultsChangeType0x0switch 没有匹配的 case 所以没有问题,但这仍然需要注意。
    • @AdamS 我很震惊 Swift 执行的 case 匹配提供的 switch 值。
    • 同意,这种行为令人担忧。编译器必须强制开关/案例是详尽无遗的 - 如果它不是(并且不能在编译时被捕获,如此处),那应该是一个运行时错误,或者至少它不应该'不执行任何案件!
    【解决方案4】:

    问题的发生是因为重新加载和删除相同的indexPath(这是苹果产生的一个错误),所以我改变了处理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(节和行都相等)时,我重新加载单元格
    [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

    【讨论】:

      【解决方案5】:

      其他答案对我来说很接近,但我收到“ (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 列出的路径相同),这只是检查它是否是正确的“插入”并跳过其他的。

      【讨论】:

        【解决方案6】:

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

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

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

        我仍然认为最好的选择是直接致电reloadData

        我相信你可以不费吹灰之力地调整我的代码,我这里有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 仍在发送,而不是“更新”。

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

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

        【讨论】:

        • 我们仍然可以使用 XCode 7 GM 重现此错误
        • 奇怪的是,在使用 Xcode 7 GM 种子编译后,我只在 iOS 8.x 设备/模拟器上看到了这个。 iOS 9 模拟器对我来说不再有问题了……
        • @Andy 我将您的代码转换为 Swift 并将其作为要点分享(当然要归功于您):gist.github.com/JohnEstropia/d7b25c11ba15564f0b16
        • @Brian 哈哈!在 iOS 8.4 的模拟器中查看它。 FRC 完全坏了。
        • 在针对最新的 iOS 9 SDK 构建时,在 iOS 8 中仍然遇到此问题。解决方法非常痛苦(将 FRC 与 UICollectionView 配对时更糟)我几乎倾向于进行操作系统检查并为 iOS 8 重新加载数据,仅为 iOS 9 保留插入/更新/移动/删除动画
        猜你喜欢
        • 2016-01-12
        • 2015-06-02
        • 2021-10-30
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多