【问题标题】:NSFetchedResultsController crash when Section index changes当 Section 索引更改时,NSFetchedResultsController 崩溃
【发布时间】:2016-11-20 04:38:32
【问题描述】:

我正在 Xcode 8 中使用 Swift 3(转换后的)编写我的应用程序。

NSFetchedResultsController 对我造成了严重的应用程序错误。

当用户使用日期选择器更改“事件日期”时,我的主表视图由一个名为“yearText”的文本标识符划分,该标识符在任何给定的事件记录 (NSManagedObject) 上设置。更改或解除选择器时,将从日期中删除该年,转换为文本,并存储在事件对象中。然后保存托管对象上下文。

如果选择的日期已经存在一个部分(即年份“2020”),则会引发错误:

[error] 错误:严重的应用程序错误。在调用 -controllerDidChangeContent: 期间,从 NSFetchedResultsController 的委托中捕获了异常。无效更新:第 0 节中的行数无效。更新后现有节中包含的行数 (2) 必须等于更新前该节中包含的行数 (1),加上或减去数字从该部分插入或删除的行数(0 插入,0 删除)加上或减去移入或移出该部分的行数(0 移入,0 移出)。与 userInfo (null)

只要选择的日期不在已经有一个以它命名的部分的一年内,一切正常。

这是我更新数据库和tableview的相关代码:

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 生成代码。

【问题讨论】:

  • 你能告诉我们将事件插入上下文的代码吗?
  • 插入新事件可以正常工作。特定事件的编辑是通过将事件对象和托管对象上下文传递给另一个视图控制器来完成的,用户可以在其中编辑事件数据。当他们编辑日期时,有时 FetchedResultsControllerDelegate 函数会崩溃,如上所述。从进一步的测试来看,当用户更改 event.date 数据并保存上下文时,controllerWillChangeContent 似乎没有触发,因此 tableview.beginUpdates() 也没有触发。但并非总是如此。仅当移动对象时才会更改任何给定部分中的对象数
  • 你的 managedObjectContext 在主线程上运行,对吗?
  • 我没有做任何事情使它不这样做。在大多数情况下,我使用的是 Apple 自己的 Master-Detail 模板中的样板代码。我什至创建了一个新的空白 Master-Detail 进行比较,我的所有 tableView 和 fetchedResultsController 代码都是相同的。
  • 如果你能在github上分享你的项目就好了

标签: swift uitableview core-data nsfetchedresultscontroller sections


【解决方案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 用于更新案例就可以了。

【讨论】:

  • 我做了这个改变,它仍然会导致同样的错误。你的使用部分吗?
  • 是的,我使用部分。我的情况是,在我更改对象后,它会从一个部分移动到另一个部分。所以,“节中的行数”函数中的旧节会出现错误。
  • 提醒一下,在您的表格视图控制器中,如果您通过核心数据获取结果控制器处理数据源,您不需要自己更新数据,任何删除或插入命令都会导致部分错误。
  • 我查看了您的代码,发现在您的获取结果控制器初始化中,“sectionNameKeyPath”参数是“yearText”,但排序参数是“日期”。这是错误的,如果你使用核心数据段函数,第一个排序信息必须与sectionNameKeyPath相同,你可以使用其他排序信息作为排序数组中的第二个或第三个值。换个试试,祝你好运!
  • 天啊。经过 60 小时的重写和故障排除,这是一个错误。把手放下。谢谢!
猜你喜欢
  • 1970-01-01
  • 2012-03-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-09-08
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多