Fatal error: UnsafeRawBufferPointer with negative count on deleting object(s) from Core Data/NSFetchedResultsController

Hi everyone, It is the first time for me working with AppKit and a NSTableView. It is backed by a NSFetchedResultsController for Core Data. When there are changes to the dataset, a Notification is being sent:

func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
    guard let links = self.fetchedResultsController.fetchedObjects else {
        return
    }

    if self.viewContext.hasChanges {
        try? self.viewContext.save()
    }
        
    self._links = links
    NotificationCenter.default.post(name: .reloadLinkList, object: nil)
}
NotificationCenter.default.publisher(for: .reloadLinkList).receive(on: RunLoop.main).sink { notification in
    self.list.tableView.reloadData()
    self.list.linksModel.selectedRows = IndexSet([])
}
.store(in: &cancellables)

This works great for inserting and updating data. When I try to delete something, I get:

Thread 1: Fatal error: UnsafeRawBufferPointer with negative count

The code for deleting selected objects looks as following:

// selector methode for UIMenuItem`s action
@objc func onDeleteSelectedLinks(_ sender: AnyObject) {
    list.linksModel.deleteLinks(links: selectedLinks)
}
// Method for deleting links from the view context
func deleteLinks(links: [LBLink]) {
    for link in links {
        self.viewContext.delete(link)
    }
}

Thank you for any help in advance!

Accepted Reply

The answer to the following question did the trick for me: https://stackoverflow.com/questions/55976212/why-does-nstableview-crash-when-processing-deleted-rows-as-nsfetchedresultscontr

func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    rowDeletes.removeAll()
    rowInserts.removeAll()
    rowUpdates.removeAll()
}
    
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
    guard let links = self.fetchedResultsController.fetchedObjects else {
        return
    }
        
    self._links = links
        
    switch type {
    case .insert:
        rowInserts.append(newIndexPath!)
    case .delete:
        rowDeletes.append(indexPath!)
    case .move:
        rowDeletes.append(indexPath!)
        rowInserts.append(newIndexPath!)
    case .update:
        rowUpdates.append(newIndexPath!)
    @unknown default:
        return
    }
}
    
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    rowDeletes = rowDeletes.sorted { $0.item > $1.item }
    rowInserts = rowInserts.sorted { $0.item < $1.item }
    rowUpdates = rowUpdates.sorted { $0.item < $1.item }
        
    NotificationCenter.default.post(name: .reloadLinkList, object: nil)
}
NotificationCenter.default.publisher(for: .reloadLinkList).receive(on: RunLoop.main).sink { notification in
    self.list.tableView.beginUpdates()
    self.list.tableView.removeRows(at: IndexSet(self.list.linksModel.rowDeletes.map {
        return $0.item
    }))
    self.list.tableView.insertRows(at: IndexSet(self.list.linksModel.rowInserts.map {
        return $0.item
    }))
    self.list.tableView.endUpdates()
                
    self.list.tableView.reloadData()
    self.list.linksModel.selectedRows = IndexSet([])
}
.store(in: &cancellables)

Replies

The answer to the following question did the trick for me: https://stackoverflow.com/questions/55976212/why-does-nstableview-crash-when-processing-deleted-rows-as-nsfetchedresultscontr

func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    rowDeletes.removeAll()
    rowInserts.removeAll()
    rowUpdates.removeAll()
}
    
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
    guard let links = self.fetchedResultsController.fetchedObjects else {
        return
    }
        
    self._links = links
        
    switch type {
    case .insert:
        rowInserts.append(newIndexPath!)
    case .delete:
        rowDeletes.append(indexPath!)
    case .move:
        rowDeletes.append(indexPath!)
        rowInserts.append(newIndexPath!)
    case .update:
        rowUpdates.append(newIndexPath!)
    @unknown default:
        return
    }
}
    
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    rowDeletes = rowDeletes.sorted { $0.item > $1.item }
    rowInserts = rowInserts.sorted { $0.item < $1.item }
    rowUpdates = rowUpdates.sorted { $0.item < $1.item }
        
    NotificationCenter.default.post(name: .reloadLinkList, object: nil)
}
NotificationCenter.default.publisher(for: .reloadLinkList).receive(on: RunLoop.main).sink { notification in
    self.list.tableView.beginUpdates()
    self.list.tableView.removeRows(at: IndexSet(self.list.linksModel.rowDeletes.map {
        return $0.item
    }))
    self.list.tableView.insertRows(at: IndexSet(self.list.linksModel.rowInserts.map {
        return $0.item
    }))
    self.list.tableView.endUpdates()
                
    self.list.tableView.reloadData()
    self.list.linksModel.selectedRows = IndexSet([])
}
.store(in: &cancellables)