iOS 9 - "尝试删除并重新加载相同的索引路径"

Bar*_*zyk 43 nsfetchedresultscontroller ios swift ios9

这是一个错误:

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()
}
Run Code Online (Sandbox Code Playgroud)

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()
    }
}
Run Code Online (Sandbox Code Playgroud)

Mar*_*n R 28

这似乎是iOS 9中的一个错误(仍然是测试版),也在Apple开发者论坛中进行了讨论

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

indexPath != newIndexPath 在上面的线程中添加显式检查似乎可以解决问题:

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

  • 仍然发生在XCode 7 GM构建上! (6认同)
  • 你能真正比较2个NSIndexPath实例与"indexPath!= newIndexPath"的相等性吗?你不需要比较行和部分吗?或者使用NSIndexPath.compare:?或者也许isEqual? (2认同)

Ben*_*air 20

更新:在针对iOS 9.0或iOS 9.1(beta)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;
    }
}
Run Code Online (Sandbox Code Playgroud)

仅限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;

        // ...
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 在构建最新的iOS 9 SDK时,iOS 8中仍会遇到此问题.解决方法非常痛苦(在将FRC与UICollectionView配对时更糟糕)我几乎倾向于进行操作系统检查,只需为iOS 8重新加载数据,仅为iOS 9保留插入/更新/移动/删除动画 (3认同)
  • 我们仍然可以使用XCode 7 GM重现这个错误 (2认同)
  • 奇怪的是,在使用Xcode 7 GM种子编译后,我只在iOS 8.x设备/模拟器上看到了这一点.iOS 9模拟器不再对我有问题...... (2认同)

mag*_*gma 16

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

NSFetchedResultsChangeType枚举有四个可能的值(与价值观的意见是我的):

public enum NSFetchedResultsChangeType : UInt {
    case Insert // 1
    case Delete // 2
    case Move   // 3
    case Update // 4
}
Run Code Online (Sandbox Code Playgroud)

..但是,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)
        }
    }
Run Code Online (Sandbox Code Playgroud)

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

无效更新:第0节中的行数无效.更新(7)后现有部分中包含的行数必须等于更新前该部分中包含的行数(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)
        }
    }
Run Code Online (Sandbox Code Playgroud)

另一种选择是检查type.rawValue无效值.

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

致谢:这最初是由iCN7发现的,来源:Apple开发者论坛 - iOS 9 CoreData NSFetchedResultsController更新导致UICollectionView/UITableView中的空白行

  • @AdamS我很震惊,Swift执行一个`case`,它不匹配提供的`switch`值. (3认同)

Bar*_*zyk 10

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

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

仅在iOS 8.x下发生

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

最简单的修复:

以下部分代码:

case .Update:
    if let indexPath = indexPath {
        tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
    }
Run Code Online (Sandbox Code Playgroud)

用...来代替:

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
        }
    }
Run Code Online (Sandbox Code Playgroud)

真的很棒.