web-dev-qa-db-fra.com

iOS 9 - "tentative de suppression et de rechargement du même chemin d'index"

C'est une erreur:

CoreData: erreur: erreur d'application grave. Une exception a été interceptée du délégué de NSFetchedResultsController lors d'un appel à -controllerDidChangeContent :. tentative de suppression et de rechargement du même chemin d'index ({longueur = 2, chemin = 0 - 0}) avec userInfo (null)

C'est ma NSFetchedResultsControllerDelegate typique:

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

dans 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()
    }
}
43

Pour une raison quelconque, NSFetchedResultsController appelle .Update suivi de .Move après que controllerWillChangeContent: a été appelé.

Cela ressemble simplement à ceci: BEGIN UPDATES ->UPDATE->MOVE-> END UPDATES .

Se produit uniquement sous iOS 8.x

Pendant une session de mise à jour, la même cellule est rechargée et supprimée, ce qui provoque un blocage. 

LE PLUS SIMPLE FIX JAMAIS:

La partie de code suivante:

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

remplacer par:

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

CELA FONCTIONNE VRAIMENT .

9

Cela semble être un bug dans iOS 9 (qui est toujours en version bêta) et est également discuté dans le forum des développeurs Apple.

Je peux confirmer le problème avec le simulateur iOS 9 de Xcode 7 beta 3 . J'ai observé que, pour un objet géré mis à jour, la méthode de délégation didChangeObject: est appelée deux fois: une fois avec l'événement NSFetchedResultsChangeUpdate, puis de nouveau avec l'événement NSFetchedResultsChangeMove (et indexPath == newIndexPath). 

Ajouter une vérification explicite pour indexPath != newIndexPathas suggéré dans le fil de discussion ci-dessus semble résoudre le problème:

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

Mise à jour: le problème décrit ne se produit que sur iOS 8 lors de la compilation avec le SDK iOS 9.0 ou iOS 9.1 (bêta).

Après avoir joué avec Xcode 7 beta 6 (iOS 9.0 beta 5), ​​j’ai eu une solution de rechange horrible aujourd’hui et il semble que cela fonctionne.

Vous ne pouvez pas utiliser reloadRowsAtIndexPaths car, dans certains cas, il est appelé trop tôt et peut entraîner des incohérences. Vous devez plutôt mettre à jour manuellement votre cellule.

Je pense toujours que la meilleure option consiste simplement à appeler reloadData

Je crois que vous pouvez adapter mon code pour Swift sans effort, j'ai le projet Objective-c ici.

@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 uniquement

Dans Xcode 7/iOS 9.0, NSFetchedResultsChangeMove est toujours envoyé au lieu de "mettre à jour".

Pour contourner le problème, désactivez simplement les animations pour ce cas:

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

        // ...
    }
}
20
highmaintenance

En ce qui concerne ce qui se passe sur iOS8, avec les compilations compilées sur iOS9, en plus du problème indexPath==newIndexPath abordé par d'autres réponses, il se passe quelque chose d'autre qui est très étrange.

La variable NSFetchedResultsChangeType a quatre valeurs possibles (les commentaires avec les valeurs sont les miens):

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

.. cependant, la fonction controller:didChangeObject:atIndexPath:forChangeType est parfois appelée avec une valeur invalide 0x0.

Swift semble passer par défaut au premier cas switch à ce moment-là, donc si vous avez la structure suivante:

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

.. l'appel invalide donnera lieu à une insertion et vous obtiendrez une erreur comme celle-ci:

Mise à jour non valide: nombre de lignes non valide dans la section 0. Le nombre de les lignes contenues dans une section existante après la mise à jour (7) doivent être égal au nombre de lignes contenues dans cette section avant le update (7), plus ou moins le nombre de lignes insérées ou supprimées de cette section (1 inséré, 0 supprimé)

Échanger simplement les cas pour que le premier cas soit une mise à jour plutôt inoffensive résout le problème:

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

Une autre option serait de vérifier type.rawValue pour une valeur non valide.

Note: bien que cela adresse un message d'erreur légèrement différent de celui envoyé par l'OP, le problème est lié; il est peu probable que dès que vous résolvez le problème indexPath==newIndexPath, celui-ci apparaîtra .. .. De plus, les blocs de code ci-dessus sont simplifiés pour illustrer la séquence; Par exemple, les blocs guard appropriés sont manquants - veuillez ne pas les utiliser tels quels.

Credits: ceci a été découvert par iCN7, source: Forums des développeurs Apple - La mise à jour pour iOS 9 CoreData NSFetchedResultsController entraîne l'apparition de lignes vides dans UICollectionView/UITableView

16
magma

Les autres réponses étaient proches pour moi, mais je recevais "<invalid> (0x0)" en tant que NSFetchedResultsChangeType. J'ai remarqué que cela était interprété comme un changement "d'insertion". Donc, le correctif suivant a fonctionné pour moi:

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

Comme chaque "insert" ne revient qu'avec un newIndexPath et aucun indexPath (et cet étrange appel de délégué d'insertion supplémentaire revient avec le même chemin que celui indiqué pour newIndexPath et indexPath), ceci vérifie simplement que c'est le bon type "d'insertion" et saute les autres.

2
Brandon Roberts

Le problème est dû au rechargement et à la suppression du même indexPath (ce qui est un bogue produit par Apple). Je change donc la façon dont je gère le message NSFetchedResultsChangeUpdate.

Au lieu de:

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

J'ai mis à jour le contenu de la cellule manuellement:

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

Cela fonctionne bien.

BTW: La mise à jour de l’objet CoreData produirait une suppression et un message d’insertion. Pour mettre à jour correctement le contenu de la cellule, lorsque la indexPath est égale à la newIndexPath (la section et la ligne sont égales), je recharge la cellule par
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];

Voici l exemple de code:

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

    }
}

J'ai mis l'exemple de code ci-dessus dans Gist: https://Gist.github.com/dreamolight/157266c615d4a226e772

0
Stan