wake-up-neo.net

iOS 9 - "Versuch, denselben Indexpfad zu löschen und neu zu laden"

Dies ist ein Fehler:

CoreData: Fehler: Schwerwiegender Anwendungsfehler. Der Delegat von NSFetchedResultsController hat während eines Aufrufs von -controllerDidChangeContent: eine Ausnahme abgefangen. Versuchen Sie, denselben Indexpfad ({length = 2, path = 0 - 0}) mit userInfo (null) zu löschen und erneut zu laden.

Dies ist meine typische 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()
    }
}

Aus irgendeinem Grund NSFetchedResultsController ruft .Update gefolgt von .Move auf, nachdem controllerWillChangeContent: aufgerufen wurde.

Es sieht einfach so aus: BEGIN UPDATES ->UPDATE->MOVE-> END UPDATES .

Passiert nur unter iOS 8.x

Während einer Aktualisierungssitzung wird dieselbe Zelle erneut geladen und gelöscht, wodurch ein Absturz verursacht wird. 

DER EINFACHSTE FIX EVER:

Der folgende Teil des Codes:

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

ersetzen mit:

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

das funktioniert wirklich .

Dies scheint ein Fehler in iOS 9 zu sein (der noch im Beta-Stadium ist) und wird auch im Apple Developer Forum diskutiert

Ich kann das Problem mit dem iOS 9-Simulator von Xcode 7 Beta 3 bestätigen. Ich beobachtete, dass für ein aktualisiertes verwaltetes Objekt die Delegat-Methode didChangeObject: zweimal aufgerufen wird: Einmal mit dem Ereignis NSFetchedResultsChangeUpdate und dann erneut mit dem NSFetchedResultsChangeMove event (und indexPath == newIndexPath). 

Das Hinzufügen einer expliziten Prüfung für indexPath != newIndexPathas, wie im obigen Thread vorgeschlagen, scheint das Problem zu lösen:

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

Update: Das beschriebene Problem tritt nur unter iOS 8 auf, wenn gegen iOS 9.0 oder iOS 9.1 (Beta) SDK erstellt wird.

Nach dem Spielen mit Xcode 7 Beta 6 (iOS 9.0 Beta 5) habe ich heute eine schreckliche Problemumgehung gefunden und es scheint, dass es funktioniert.

Sie können reloadRowsAtIndexPaths nicht verwenden, da dies in bestimmten Fällen zu früh aufgerufen wird und Inkonsistenzen verursachen kann. Stattdessen sollten Sie Ihre Zelle manuell aktualisieren.

Ich denke immer noch, dass die beste Möglichkeit ist, einfach reloadData aufzurufen. 

Ich glaube, Sie können meinen Code ohne Mühe an Swift anpassen. Ich habe hier ein objektives Projekt.

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

nur Xcode 7/iOS 9.0

In Xcode 7/iOS 9.0 wird NSFetchedResultsChangeMove immer noch anstelle von "Update" gesendet.

Deaktivieren Sie als einfache Problemumgehung Animationen für diesen Fall:

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

In Bezug auf iOS8 mit Builds, die für iOS9 kompiliert wurden}, zusätzlich zu dem indexPath==newIndexPath-Problem, auf das sich einige andere Antworten beziehen, etwas anderes passiert, was sehr seltsam ist.

Das NSFetchedResultsChangeType-Enum hat vier mögliche Werte (Kommentare mit Werten sind meine):

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

.. Die Funktion controller:didChangeObject:atIndexPath:forChangeType wird jedoch manchmal mit einem ungültigen Wert 0x0 aufgerufen.

Swift scheint an diesem Punkt den ersten switch-Fall zu verwenden, wenn Sie also die folgende Struktur haben:

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

.. der ungültige Aufruf führt zu einer Einfügung, und Sie erhalten einen Fehler wie den folgenden:

Ungültige Aktualisierung: ungültige Anzahl von Zeilen in Abschnitt 0. Die Anzahl der Zeilen, die in einem vorhandenen Abschnitt nach dem Update (7) enthalten sind, müssen .__ sein. gleich der Anzahl der Zeilen in diesem Abschnitt vor dem Update (7) plus oder minus der Anzahl der Zeilen, die aus .__ eingefügt oder gelöscht wurden. dieser Abschnitt (1 eingefügt, 0 gestrichen)

Tauschen Sie einfach die Fälle aus, sodass der erste Fall ein ziemlich harmloses Update ist. Das Problem wird dadurch behoben:

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

Eine andere Option wäre, type.rawValue auf einen ungültigen Wert zu prüfen.

Hinweis: Während hier eine geringfügig andere Fehlermeldung als die vom OP ausgegeben wird, ist das Problem damit verbunden. Es ist ziemlich wahrscheinlich, dass sobald Sie das indexPath==newIndexPath-Problem behoben haben, dieses aufklappt . Außerdem werden die obigen Codeblöcke vereinfacht, um die Reihenfolge zu veranschaulichen. Zum Beispiel fehlen die entsprechenden guard-Blöcke - bitte verwenden Sie sie nicht so, wie sie sind.

Credits: Dies wurde ursprünglich von iCN7 entdeckt. Quelle: Apple-Entwicklerforen - iOS 9 CoreData NSFetchedResultsController-Update bewirkt leere Zeilen in UICollectionView/UITableView

16
magma

Die anderen Antworten waren für mich nahe, aber ich erhielt "<invalid> (0x0)" als NSFetchedResultsChangeType. Ich bemerkte, dass es als "Einfügungsänderung" interpretiert wurde. Der folgende Fix hat also für mich funktioniert:

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

Da jedes "Einfügen" nur mit einem newIndexPath und keinem indexPath zurückkommt (und dieser seltsame zusätzliche Delegatenaufruf mit dem gleichen Pfad für newIndexPath und indexPath aufgeführt wird), wird nur überprüft, ob es sich um die richtige Art des "Einfügens" handelt überspringt die anderen.

2
Brandon Roberts

Das Problem ist auf das erneute Laden und Löschen des gleichen Indexpfads zurückzuführen (den von Apple erzeugten Fehler). Daher ändere ich die Art und Weise, wie ich die NSFetchedResultsChangeUpdate-Nachricht handle.

Anstatt:

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

Ich habe den Inhalt der Zelle manuell aktualisiert:

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

Es stellt sich heraus, dass es gut funktioniert.

BTW: Das Update des CoreData-Objekts würde eine Lösch- und eine Einfügemeldung erzeugen. Um den Zellinhalt korrekt zu aktualisieren, wenn indexPath gleich dem newIndexPath ist (sowohl der Abschnitt als auch die Zeile sind gleich), lade ich die Zelle neu
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];

Hier ist der Beispielcode:

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

    }
}

Ich habe den obigen Beispielcode auf Gist gesetzt: https://Gist.github.com/dreamolight/157266c615d4a226e772

0
Stan