Почему происходит сбой NSTableView при обработке удаленных строк как NSFetchedResultsControllerDelegate? - PullRequest
7 голосов
/ 03 мая 2019

Я использую довольно стандартную настройку NSTableView + CoreData + NSFetchedResultsController, а соответствующий контроллер представления - NSFetchedResultsControllerDelegate для получения изменений. Вот соответствующие биты кода из контроллера представления:

func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?){

    print("Change type \(type) for indexPath \(String(describing: indexPath)), newIndexPath \(String(describing: newIndexPath)). Changed object: \(anObject). FRC by this moment has \(String(describing: self.frc?.fetchedObjects?.count)) objects, tableView has \(self.tableView.numberOfRows) rows")

    switch type {
    case .insert:
        if let newIndexPath = newIndexPath {
            tableView.insertRows(at: [newIndexPath.item], withAnimation: .effectFade)
        }
    case .delete:
        if let indexPath = indexPath {
            tableView.removeRows(at: [indexPath.item], withAnimation: .effectFade)
        }
    case .update:
        if let indexPath = indexPath {
            let row = indexPath.item
            for column in 0..<tableView.numberOfColumns {
                tableView.reloadData(forRowIndexes: IndexSet(integer: row), columnIndexes: IndexSet(integer: column))
            }
        }
    case .move:
        if let indexPath = indexPath, let newIndexPath = newIndexPath {
            tableView.removeRows(at: [indexPath.item], withAnimation: .effectFade)
            tableView.insertRows(at: [newIndexPath.item], withAnimation: .effectFade)
        }
    @unknown default:
        fatalError("Unknown fetched results controller change result type")
    }
}

func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    print("tableViewBeginUpdates")
    tableView.beginUpdates()
}

func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    tableView.endUpdates()
    print("tableViewEndUpdates")
}

Я понимаю, что смогу пакетировать все обновления таким образом, даже если удалено несколько строк. Однако это приводит к сбою с несколькими удалениями подряд.

Вот вывод журнала из сеанса с таблицей, изначально имеющей четыре строки, и все они удаляются:

tableViewBeginUpdates
Change type NSFetchedResultsChangeType for indexPath Optional([0, 2]), newIndexPath nil. Changed object: /… correct object info …/. FRC by this moment has Optional(0) objects, tableView has 4 rows
Change type NSFetchedResultsChangeType for indexPath Optional([0, 1]), newIndexPath nil. Changed object: /… correct object info …/. FRC by this moment has Optional(0) objects, tableView has 3 rows
Change type NSFetchedResultsChangeType for indexPath Optional([0, 0]), newIndexPath nil. Changed object: /… correct object info …/. FRC by this moment has Optional(0) objects, tableView has 2 rows
Change type NSFetchedResultsChangeType for indexPath Optional([0, 3]), newIndexPath nil. Changed object: /… correct object info …/. FRC by this moment has Optional(0) objects, tableView has 1 rows

Последняя строка вызывает сбой:

2019-05-06 22:01:30.968849+0300 MyApp[3517:598234] *** Terminating app due to uncaught exception 'NSTableViewException', reason: 'NSTableView error inserting/removing/moving row 3 (numberOfRows: 1).'

Первые три удаления сообщаются в «правильном» порядке (строки с большими индексами [номера строк] удаляются первыми). Последний прибывает «не по порядку», а другие строки, по-видимому, уже вышли из NSTableView к этому времени.

Как объекты в первую очередь удаляются из контекста: Я использую рекомендованную лучшую практику, чтобы два контекста управляемых объектов работали с одним и тем же NSPersistentContainer, один для работы пользовательского интерфейса в основном потоке, и один для фоновой / сетевой работы в фоновом режиме. Они следят за изменениями друг друга. Этот сбой вызывается, когда контекст синхронизации получает некоторые изменения из сети, сохраняет их, и они распространяются для просмотра контекста с помощью этого метода в другом месте приложения:

@objc func syncContextDidSave(note: NSNotification) {
    viewContext.perform {
        self.viewContext.mergeChanges(fromContextDidSave: note as Notification)
    }
}

Я неправильно понял, как работать с полученным делегатом контроллера результатов? Я думал, что вызовы beginupdates / endupdates удостоверяются, что «модель табличного представления» не изменяется между ними? Что я должен сделать, чтобы устранить аварию?

Ответы [ 2 ]

2 голосов
/ 13 мая 2019

Обновление из fetchedResultsController сложнее, чем указано в документации Apple.Код, которым вы делитесь, будет вызывать такого рода ошибку, когда происходит перемещение и вставка или перемещение и удаление одновременно.Это не похоже на то, что происходит в вашем случае, но эта установка также исправит это.

indexPath - это индекс ДО того, как будут применены удаления и вставки;newIndexPath - индекс ПОСЛЕ удаления и вставки.

Для обновлений вам все равно, где они были ДО вставок и удалите - только после - используйте newIndexPath, а не indexPath.Это исправит сбои, которые могут произойти, когда вы обновляете и вставляете (или обновляете и удаляете) одновременно, и ячейка не обновляется, как вы ожидаете.

Для move делегат говорит, где онперемещено ДО ДО вставок и куда это должно быть вставлено ПОСЛЕ вставок и удаляет.Это может быть сложным, когда у вас есть перемещение и вставка (или перемещение и удаление).Вы можете исправить это, сохранив все изменения из controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: в три разных массива, вставьте, удалите и обновите (вы можете использовать пользовательский объект или словарь - все, что вам подходит).Когда вы получите move, добавьте для него запись как в массиве вставки, так и в массиве удаления.В controllerDidChangeContent: сортируйте массив удаления по убыванию и вставку массива по возрастанию.Затем примените изменения - сначала удалите, затем вставьте, затем обновите.Это исправит сбои, которые могут произойти, если у вас есть перемещение и вставка (или перемещение и удаление) одновременно.

Я не могу объяснить, почему вы удалили не по порядку.В моем тестировании я всегда видел, как удаленные файлы обслуживаются по убыванию, а вставки - по возрастанию.Тем не менее, эта установка также исправит ваши проблемы, так как есть шаг для сортировки удалений.

Если у вас есть разделы, сохраните изменения разделов в массивах, а затем примените изменения по порядку: удаляет (убывает)), sectionDelete (по убыванию), sectionInserts (по возрастанию), вставки (по возрастанию), обновления (в любом порядке).Разделы не могут быть перемещены или обновлены.

Сводка:

  1. Имеет 5 массивов: sectionInserts, sectionDeletes, rowDeletes, rowInserts и rowUpdates
  2. в controllerWillChangeContent очистить всемассивы
  3. в контроллере: didChangeObject: добавление indexPaths в массивы (перемещение - это удаление и вставка. Обновление использует newIndexPath)
  4. в контроллере: didChangeSection добавляет раздел в sectionInserts или rowDeletesмассив
  5. в controllerDidChangeContent: обработайте их следующим образом:

    • sort rowDeletes по убыванию
    • sort sectionDelete по убыванию
    • sort section вставляет по возрастанию
    • сортировка rowInserts по возрастанию
  6. , затем в одном блоке executeBatchUpdates применить изменения к collectionView: rowDeletes, sectionDelete, sectionInserts, rowInserts и rowUpdates в указанном порядке.

2 голосов
/ 10 мая 2019

Надеюсь, здесь вам немного поможет официальная документация по операциям пакетного удаления в UITableView.

В приведенном ниже примере операции удаления всегда выполняются первыми, откладывая операции удаления, но идея в том, чтоВы фиксируете их все одновременно между началом и концом, так что UITableView может выполнить всю тяжелую работу за вас.

- (IBAction)insertAndDeleteRows:(id)sender {
    // original rows: Arizona, California, Delaware, New Jersey, Washington

    [states removeObjectAtIndex:4]; // Washington
    [states removeObjectAtIndex:2]; // Delaware
    [states insertObject:@"Alaska" atIndex:0];
    [states insertObject:@"Georgia" atIndex:3];
    [states insertObject:@"Virginia" atIndex:5];

    NSArray *deleteIndexPaths = [NSArray arrayWithObjects:
                                [NSIndexPath indexPathForRow:2 inSection:0],
                                [NSIndexPath indexPathForRow:4 inSection:0],
                                nil];
    NSArray *insertIndexPaths = [NSArray arrayWithObjects:
                                [NSIndexPath indexPathForRow:0 inSection:0],
                                [NSIndexPath indexPathForRow:3 inSection:0],
                                [NSIndexPath indexPathForRow:5 inSection:0],
                                nil];
    UITableView *tv = (UITableView *)self.view;

    [tv beginUpdates];
    [tv insertRowsAtIndexPaths:insertIndexPaths withRowAnimation:UITableViewRowAnimationRight];
    [tv deleteRowsAtIndexPaths:deleteIndexPaths withRowAnimation:UITableViewRowAnimationFade];
    [tv endUpdates];

    // ending rows: Alaska, Arizona, California, Georgia, New Jersey, Virginia
}

В этом примере удаляются две строки из массива (и их соответствующиестрок) и вставляет три строки в массив (вместе с соответствующими им строками).В следующем разделе, Порядок операций и пути к индексам, объясняются конкретные аспекты поведения вставки и удаления строки (или раздела).

Ключевым моментом здесь является то, что все индексы удаления передаются одновременномежду начальным и конечным вызовами обновления.Если вы сначала сохраните индексы, а затем передадите их, то попадете в ситуацию, о которой я упоминал в своем комментарии, когда индексы начинают выходить за пределы исключений.

Документацию по яблоку можно найти здесь и приведенный выше образец под заголовком: «Пример операций пакетной вставки и удаления»

Надеюсь, это поможет вам указать правильное направление.

...