Сбой NSFetchedResultsController при изменении индекса раздела

Я пишу свое приложение на Swift 3 (преобразованное) в Xcode 8.

NSFetchedResultsController вызывает у меня серьезную ошибку приложения.

Мое основное табличное представление разделено текстовым идентификатором под названием «yearText», который устанавливается для любой данной записи события (NSManagedObject), когда пользователь изменяет «Дату события» с помощью средства выбора даты. Когда средство выбора изменяется или закрывается, год удаляется из даты, преобразуется в текст и сохраняется в объекте Event. Контекст управляемого объекта затем сохраняется.

Если выбрана дата, для которой уже существует раздел (например, год «2020»), выдается ошибка:

[ошибка] ошибка: Серьезная ошибка приложения. Исключение было перехвачено делегатом NSFetchedResultsController во время вызова -controllerDidChangeContent:. Недопустимое обновление: недопустимое количество строк в разделе 0. Количество строк, содержащихся в существующем разделе после обновления (2), должно быть равно количеству строк, содержащихся в этом разделе до обновления (1), плюс или минус число строк, вставленных или удаленных из этого раздела (0 вставленных, 0 удаленных) и плюс или минус количество строк, перемещенных в этот раздел или из него (0 добавленных, 0 удаленных). с информацией о пользователе (нулевой)

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

Вот мой соответствующий код для обновления базы данных и таблицы:

var fetchedResultsController: NSFetchedResultsController<NSFetchRequestResult> {
    if _fetchedResultsController != nil {
        return _fetchedResultsController!
    }

    // Fetch the default object (Event)
    let fetchRequest = NSFetchRequest<NSFetchRequestResult>()
    let entity = NSEntityDescription.entity(forEntityName: "Event", in: managedObjectContext!)
    fetchRequest.entity = entity

    // Set the batch size to a suitable number.
    fetchRequest.fetchBatchSize = 60

    // Edit the sort key as appropriate.
    let sortDescriptor = NSSortDescriptor(key: "date", ascending: false)

    fetchRequest.sortDescriptors = [sortDescriptor]

    // Edit the section name key path and cache name if appropriate.
    // nil for section name key path means "no sections".
    let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext!, sectionNameKeyPath: "yearText", cacheName: nil)
    aFetchedResultsController.delegate = self
    _fetchedResultsController = aFetchedResultsController

    do {
        try _fetchedResultsController!.performFetch()
    } catch {
         // Implement error handling code here.
         abort()
    }

    return _fetchedResultsController!
}    
var _fetchedResultsController: NSFetchedResultsController<NSFetchRequestResult>?


// MARK: - UITableViewDelegate

    extension EventListViewController: UITableViewDelegate {

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let cell = tableView.cellForRow(at: indexPath) as! EventCell
        cell.isSelected = true
        configureCell(withCell: cell, atIndexPath: indexPath)
    }

    func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
        let cell = tableView.cellForRow(at: indexPath) as! EventCell
        cell.isSelected = false
        configureCell(withCell: cell, atIndexPath: indexPath)
    }
}


// MARK: - UITableViewDataSource

extension EventListViewController: UITableViewDataSource {

    func numberOfSections(in tableView: UITableView) -> Int {
        return fetchedResultsController.sections?.count ?? 0
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        let sectionInfo = fetchedResultsController.sections![section]
        return sectionInfo.numberOfObjects
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "EventCell", for: indexPath) as! EventCell
        configureCell(withCell: cell, atIndexPath: indexPath)
        return cell
    }

    func configureCell(withCell cell: EventCell, atIndexPath indexPath: IndexPath) {
       // bunch of stuff to make the cell pretty and display the data
    }

func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
    // Return false if you do not want the specified item to be editable.
    return true
}

func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
    if editingStyle == .delete {
        let context = fetchedResultsController.managedObjectContext
        context.delete(fetchedResultsController.object(at: indexPath) as! NSManagedObject)
        do {
            try context.save()
        } catch {
                // Replace this implementation with code to handle the error appropriately.
            // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
            //print("Unresolved error \(error), \(error.userInfo)")
            abort()
        }
    }
}

    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        let sectionInfo = fetchedResultsController.sections![section]
        return sectionInfo.name
    }

    func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
        // make the section header look good
        view.tintColor = kWPPTintColor
        let header = view as! UITableViewHeaderFooterView
        header.textLabel?.textColor = kWPPDarkColor
        header.textLabel?.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.subheadline)
    }
}


// MARK: - NSFetchedResultsControllerDelegate

extension EventListViewController: NSFetchedResultsControllerDelegate {

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

    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
        switch type {
        case .insert:
            tableView.insertSections(IndexSet(integer: sectionIndex), with: .fade)
        case .delete:
            tableView.deleteSections(IndexSet(integer: sectionIndex), with: .fade)
        default:
            return
        }
    }

    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
        switch type {
        case .insert:
            tableView.insertRows(at: [newIndexPath!], with: .fade)
        case .delete:
            tableView.deleteRows(at: [indexPath!], with: .fade)
        case .update:
            configureCell(withCell: tableView.cellForRow(at: indexPath!)! as! EventCell, atIndexPath: indexPath!)
        case .move:
            tableView.moveRow(at: indexPath!, to: newIndexPath!)
        }
    }

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

Я надеюсь, что вы можете предложить мне несколько предложений. Спасибо.

РЕДАКТИРОВАТЬ: вынул некоторый код, который только мешал, и пересмотрел .move для использования .moveRow

РЕДАКТИРОВАТЬ 2: Добавлен код генерации FRC.


person Kent    schedule 20.11.2016    source источник
comment
Можете ли вы показать нам код, который вставляет событие в контекст?   -  person ELKA    schedule 20.11.2016
comment
Вставка новых событий работает нормально. Редактирование конкретного события осуществляется путем передачи объекта события и контекста управляемого объекта другому контроллеру представления, где пользователь может редактировать данные события. Когда они редактируют дату, иногда происходит сбой функций FetchedResultsControllerDelegate, как указано выше. Из дальнейшего тестирования кажется, что когда пользователь изменяет данные event.date и контекст сохраняется, контроллерWillChangeContent, похоже, не срабатывает, и поэтому tableview.beginUpdates() также не срабатывает. Но не все время. Только при перемещении объекта изменяется количество объектов в любом заданном разделе   -  person Kent    schedule 20.11.2016
comment
ваш manageObjectContext работает в основном потоке, верно?   -  person ELKA    schedule 20.11.2016
comment
Я не сделал ничего, что заставило бы его поступить иначе. По большей части я использую шаблонный код из собственного шаблона Apple Master-Detail. Я даже создал новый пустой Master-Detail для сравнения, и мой код одинаков для всего кода tableView и fetchedResultsController.   -  person Kent    schedule 20.11.2016
comment
Было бы неплохо, если бы вы могли поделиться своим проектом на github.   -  person ELKA    schedule 20.11.2016
comment
К сожалению, это невозможно.   -  person Kent    schedule 20.11.2016
comment
Пожалуйста, покажите свое творение FRC.   -  person Mundi    schedule 21.11.2016
comment
Добавлен код для создания FRC.   -  person Kent    schedule 21.11.2016


Ответы (1)


Я столкнулся с той же ошибкой, когда обновлял некоторые свойства управляемых объектов Core Data.

Вот моя функция контроллера:

func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
    switch type {
    case .insert:
        self.tableView.insertRows(at: [newIndexPath!], with: .fade)
    case .delete:
        self.tableView.deleteRows(at: [indexPath!], with: .fade)
    case .update:
        self.tableView.reloadRows(at: [indexPath!], with: .fade)
    case .move:
        self.tableView.insertRows(at: [newIndexPath!], with: .fade)
        self.tableView.deleteRows(at: [indexPath!], with: .fade)
    }
}

Раньше я использовал newIndexPath для случая обновления, но я обнаружил, что это вызовет проблему несоответствия некоторых строк раздела, когда контроллер результатов выборки выполняет какое-либо действие обновления. Вместо этого можно использовать indexPath для случая обновления.

person tiantong    schedule 09.12.2016
comment
Я сделал это изменение, и оно по-прежнему вызывает ту же ошибку. Использует ли ваш разделы? - person Kent; 09.12.2016
comment
Да, я использую разделы. Мой случай в том, что после того, как я изменил объект, он будет перемещаться из одного раздела в другой. Таким образом, при старом разделе в количестве строк в функции раздела будет возникать ошибка. - person tiantong; 10.12.2016
comment
Напоминаем, что в контроллере табличного представления, если вы обрабатываете источник данных с помощью основного контроллера результатов выборки данных, вам НЕ нужно обновлять данные самостоятельно, любая команда удаления или вставки вызовет ошибку раздела. - person tiantong; 10.12.2016
comment
Я обрабатываю источник данных через выборку основных данных. Код приведен выше и очень прост. Я думаю, что если я изменю данные так, что запись (сущность) теперь принадлежит новому разделу, что-то пойдет не так. Например, мои разделы - это годы (т.е. 2016, 2018), но они не существуют автоматически. Когда я меняю один с 2016 на 2020, если 2020 не существует, он должен создать новый раздел. Именно тогда я получаю ошибку. Если он изменится с 2016 на 2018 год и оба уже существуют, то я не думаю, что получаю ошибку (нужно проверить еще раз). новыйIndexPath не помог. - person Kent; 11.12.2016
comment
На самом деле, нет, неважно, новый раздел или нет. Если взять один из раздела под названием 2016, вот что вызывает ошибку. Говорит, что количество предметов до и после обновления до 2016 (например) неверное. Я действительно борюсь. Мне нужно, чтобы это работало. - person Kent; 11.12.2016
comment
Эта функция выглядит корректно? func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { let sectionInfo = fetchedResultsController.sections![section] return sectionInfo.numberOfObjects } - person Kent; 11.12.2016
comment
Я просмотрел ваш код и обнаружил, что в инициализации вашего контроллера результатов выборки параметр sectionNameKeyPath — это yearText, а параметр сортировки — это дата. Это неправильно, если вы используете основную функцию раздела данных, первая информация о сортировке должна совпадать с sectionNameKeyPath, вы можете использовать другую информацию о сортировке в качестве второго или третьего значения в массиве сортировки. Измени его, попробуй, удачи! - person tiantong; 11.12.2016
comment
Я только что попробовал. Это тоже не сработало. Та же ошибка. - person Kent; 11.12.2016
comment
какую версию iOS вы использовали? Я только что видел похожую ошибку на 10.1, я думаю, что это ошибка Core Data. Когда я обновляюсь до 10.2, ошибок не возникает. - person tiantong; 18.12.2016
comment
МОЙ БОГ. После 60 часов перезаписи и устранения неполадок это ошибка. Руки вниз. Спасибо! - person Kent; 18.12.2016