NSFetchedResultsControllerDelegate animating deletion on wrong indexPath - ios

The premise: I have a UITableViewController that conforms to NSFetchedResultsControllerDelegate. I also have a fetched results controller and managed object context as variables in the controller. My tableView displays a table with one section of core data objects from the fetched results controller.
What I'm trying to implement is swipe to delete. The object selected for deletion is actually deleted, however the wrong indexPath is being animated to delete and I don't know why. I currently have the following methods that I believe are relevant:
// This method is being called in viewDidLoad, adding all of the CoreData objects to an array called fetchedResults.
func performFetch() {
do { try fetchedResultsController?.performFetch()
fetchedResults = fetchedResultsController?.fetchedObjects as! [Date]
} catch let error as NSError {
print(error.localizedDescription)
}
}
// tableViewDataSource methods
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
return true
}
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
let objectToDelete = fetchedResults[indexPath.row]
fetchedResultsController?.managedObjectContext.deleteObject(objectToDelete)
print("commitEditingStyle-indexPath = \(indexPath)")
do { try managedContext.save()
} catch let error as NSError {
print(error.localizedDescription)
}
}
}
// NSFetchedResultsControllerDelegate methods
func controllerWillChangeContent(controller: NSFetchedResultsController) {
self.tableView.beginUpdates()
}
func controller(controller: NSFetchedResultsController, didChangeObject object: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case .Delete:
if let indexPath = indexPath {
print("didChangeObject indexPath = \(indexPath)")
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
}
default:
return
}
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
self.tableView.endUpdates()
}
As you can see, I print the indexPath for the tableView:commitEditingStyle method as well as the controller:didChangeObject method. Here are the 2 print statements:
commitEditingStyle-indexPath = {length = 2, path = 0 - 3}
didChangeObject-indexPath = {length = 2, path = 0 - 0}
Why is the didChangeObject method picking up the wrong indexPath? When I swipe to delete the object, the object is deleted at the proper indexPath (in this case 3...) but the table view cell that animates deletion is indexPath 0 (the first cell in my table view). What gives?

Remove all usage of fetchedResults from your code. You are caching the initial set of objects that the FRC knows about, but you aren't tracking additions or removals in that cache. The cache is also a waste of memory because you can always get exactly what you want from the FRC and it also tracks changes.
So, what you're seeing should be what appears to be a random difference and is due to indexing differences between the cache array and the FRC. They should match initially, and if you only ever delete the last item it should be ok, but any other deletion would cause them to fall out of sync.

Related

ios application crashes when deleting line in uitableView

I have a problem in my UITableView which filled with data: [(gamme: String, [(product: String, quantity: Double)])], everything works fine: inserting rows and sections, deleting row and section, reloading.
But sometimes and when I try to delete lots of lines in fast way (line by line by swiping the line the table and tap (-) ). it leads to a crash like in the screenshot.
The issue is hard to reproduce in development app. but my clients still reports it. My clients are professionals (not normal users) and are expected to use the in a fast way with medium to large data.
and this is my func that delete lines:
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
let delete = UITableViewRowAction(style: .destructive, title: "-") { (action, indexPath) in
let cmd = self.groupedData[indexPath.section].1.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .right)
self.delegate?.didDeleteCmdLine(cmd)
if self.groupedData[indexPath.section].1.count == 0 {
self.groupedData.remove(at: indexPath.section)
tableView.deleteSections(IndexSet(integer: indexPath.section), with: UITableViewRowAnimation.right)
}
}
return [delete]
}
why is that happening ?
This is a screen of xcode organiser for the crash
Edit:
Checking if groupedData is accessed by any thread other than main proposed by #Reinhard:
private var xgroupedData = [(gamme: GammePrdCnsPrcpl, [cmdline])]()
private var groupedData: [(gamme: GammePrdCnsPrcpl, [cmdline])] {
get {
if !Thread.isMainThread {
fatalError("getting from not from main")
}
return xgroupedData
}
set {
if !Thread.isMainThread {
fatalError("setting from not from main")
}
xgroupedData = newValue
}
}
but the groupedData variable is accessed only from main thread
tableView.beginUpdates()
self.tableView.deleteRows(at: [indexPath], with: .automatic)
tableView.endUpdates()
Changes in João Luiz Fernandes answer....try this
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
objects.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
} else if editingStyle == .insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view.
}
}
reference (Hacking with swift . https://www.hackingwithswift.com/example-code/uikit/how-to-swipe-to-delete-uitableviewcells )
u can try this
var myDataArray = ["one","two","three"]
//wanna insert a row then in button action or any action write
myDataArray.append("four")
self.tblView.reloadData()
// wanna delete a row
myDataArray.removeObject("two")
// you can remove data at any specific index liek
//myDataArray.remove(at: 2)
self.tblView.reloadData()
try to use like this function.
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
print("Deleted")
// do your delete your entires here..
//
self.tableView.deleteRows(at: [indexPath], with: .automatic)
}
}
After the user tapped the delete button, you remove the corresponding row and (if this was the last row of that section) the corresponding section from your data source groupedData, which is an array. However, array operations are not thread-safe.
Can it be that another thread is using this array while it is modified by the delete action?
In this case, the app can crash. The danger is of course higher, when several actions are triggered in a short time, which seems to be the case as you described it.
One way (maybe not the best) to avoid multithreading problems is to access the array only on the main thread.
If this slows down the main thread, one can use a synchronised array that allows multiple reads concurrently, but only a single write that blocks out all reads, see here.
There is just a update #codeByThey's answer. Please Update your DataSource file as you delete that particular row.
tableView.beginUpdates()
self.whatEverDataSource.remove(at: indexPath.row)
self.tableView.deleteRows(at: [indexPath], with: .automatic)
tableView.endUpdates()
This will result that your DataSource is also update same time as the TableView. The crash is happening may be due to the DataSource is not updated.
Can you try removing the section at once instead of trying to remove the row once, and then the section which it belongs to, when the last item in a section is being removed?
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
let delete = UITableViewRowAction(style: .destructive, title: "-") { [weak self] (action, indexPath) in
// Use a weak reference to self to avoid any reference cycles.
guard let weakSelf = self else {
return
}
tableView.beginUpdates()
let cmd: cmdline
if weakSelf.groupedData[indexPath.section].1.count > 1 {
cmd = weakSelf.groupedData[indexPath.section].1.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .right)
} else {
// Remove the section when the `items` are empty or when the last item is to be removed.
cmd = weakSelf.groupedData.remove(at: indexPath.section).1[indexPath.row]
tableView.deleteSections([indexPath.section], with: .right)
}
weakSelf.delegate?.didDeleteCmdLine(cmd)
tableView.endUpdates()
}
return [delete]
}

CoreData app crashes when deleting row from TableView

I have a dozen table view controllers that all work as expected, then I have this one which crashes with:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (1) must be equal to the number of rows contained in that section before the update (1), plus or minus the number of rows inserted or deleted from that section (0 inserted, 1 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'
The code doing this has been modified several times with different options but to no effect.
import UIKit
import CoreData
class QTypeVCY: UITableViewController, NSFetchedResultsControllerDelegate
{
let app = UIApplication.shared.delegate as! AppDelegate
override func viewDidLoad()
{
super.viewDidLoad()
}
override func numberOfSections(in tableView: UITableView) -> Int
{
let sections = frc.sections
return sections!.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
guard let sections = self.frc.sections else
{
fatalError("No sections in fetchedResultsController")
}
let sectionInfo = sections[section]
return sectionInfo.numberOfObjects
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "QQQQ", for: indexPath)
let qtype = frc.object(at: indexPath)
cell.textLabel?.text = qtype.title
return cell
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete
{
do
{
let qtype = frc.object(at: indexPath)
let context = self.frc.managedObjectContext
context.delete(qtype)
try context.save()
tableView.deleteRows(at: [indexPath], with: .fade)
}
catch
{
debugPrint(error)
}
} else if editingStyle == .insert
{
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
}
}
lazy var frc: NSFetchedResultsController<Qtype> =
{
let context = self.app.persistentContainer.viewContext
let req: NSFetchRequest<Qtype> = Qtype.fetchRequest()
req.fetchBatchSize = 10
let sortDescriptor1 = NSSortDescriptor(key: #keyPath(Qtype.specialty), ascending:true)
let sortDescriptor2 = NSSortDescriptor(key: #keyPath(Qtype.title), ascending:true)
req.sortDescriptors = [sortDescriptor1, sortDescriptor2]
let afrc = NSFetchedResultsController(fetchRequest: req, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
afrc.delegate = self
do
{
try afrc.performFetch()
}
catch
{
print(error.localizedDescription)
fatalError("Abort while fetching Qtype")
}
return afrc
}()
}
The crash occurs on the tableview.deleteRows statement. I have tried surrounding the code with beginUpdate/endUpdates, with and without the performFetch, even tried re-entering the code in case I had a typo that I missed. This same basic code is working fine on other tables/view controllers, just this one.
The entity is just made up of strings. I had it with and without relationships to other entities.
The row is deleted since the next time I run the app it is missing. Also, one other thing about this table is that calling reloadData after adding a new object does not add the row. I need to leave the tableview and reenter it. I'm sure the two are related but can't say why.
Since originally posting this, I included the entire VC code instead of just the offending code. I have also tried swapping the Entity with a different entity where this issue does not occur, but the program still crashes even with a different entity.
You forgot to remove the item also from the data source array
let qtype = qtypes[indexPath.row]
let context = self.frc.managedObjectContext
context.delete(qtype)
try context.save()
qtypes.remove(at: indexPath.row) // <--
tableView.deleteRows(at: [indexPath], with: .fade)
I wish I could offer the actual solution, but the above code now works. I had updated XCode to the 1/24/18 version but I don't think that is the solution. I also stripped out a few things I had in the original code that crashed, but then I put them back and it still works. I suspect I had a typo somewhere, but after spending a half hour trying to locate it, I'm declaring victory and moving on.
I just solloved a problem similar to yours.
when deleted data in CoreData, TableView data array also delete before appDelegate.saveContext()
Code in func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath)
self.tableviewArrayData.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
appDelegate.saveContext()

Adding Edit Row Action to TableView that uses Fetch Controller?

I have a table that uses a coredata fetch to populate its self, the fetch controller stores the delete row action.
However I want to add an edit feature to the row as well, I dont seem to be able to add this along side the delete feature I currently have and need to use editActionsForRowAt to get it to appear.
When i use editActionsForRowAt it overrides by existing Fetch Controllers row actions and causes the delete to no longer work.
Any idea how I can add just an edit next to the existing delete without over writing or messing with the fetch results controller?
Here is the code for the fetch controller that is the source for the table and its delete case and delete func, I want to ideally add an edit button to this, without having to overwrite it all with editActionsForRowAt
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch (type) {
case .insert:
if let indexPath = newIndexPath {
workoutDesignerTable.insertRows(at: [indexPath], with: .fade)
}
break;
case .delete:
if let indexPath = indexPath {
workoutDesignerTable.deleteRows(at: [indexPath], with: .fade)
}
break;
case .update:
if let indexPath = indexPath, let cell = workoutDesignerTable.cellForRow(at: indexPath) as? RoutineTableViewCell {
configure(cell, at: indexPath)
}
break;
default:
print("...")
}
}
// MARK: - DELETING TABLE ROW
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let UserExercise = fetchedResultsController.managedObjectContext
UserExercise.delete(self.fetchedResultsController.object(at: indexPath))
do {
try UserExercise.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
For more information please find the below screenshot of current Table Row Edit Actions.
From the documentation tableView(_:editActionsForRowAt:)
Use this method when you want to provide custom actions for one of your table rows. When the user swipes horizontally in a row, the table view moves the row content aside to reveal your actions. Tapping one of the action buttons executes the handler block stored with the action object.
If you do not implement this method, the table view displays the standard accessory buttons when the user swipes the row.
So you need to also pass the delete action button with your other buttons.
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
let btnEdit = UITableViewRowAction(style: .default, title: "Edit") { action, indexPath in
//Put the code of edit action
}
let btnDelete = UITableViewRowAction(style: .destructive, title: "Delete") { action, indexPath in
//Put the code of delete action
}
return [btnEdit, btnDelete]
}
Note: If you are implementing tableView(_:editActionsForRowAt:) then there is no need to implement tableView(_:commit:forRowAt:) method.

How to swipe delete core data tableview in Swift 2.0

I have a table view within a VC. It is populated from a core data object using the following code:
// Variables
var allFruits: NSArray
var managedObjectContext: NSManagedObjectContext
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
func loadFruits(){
// Try to load all of the Fruits from the core data.
let fetchRequest = NSFetchRequest()
let entityDescription = NSEntityDescription.entityForName("Fruit", inManagedObjectContext: self.managedObjectContext)
fetchRequest.entity = entityDescription
do{
self.allFruits = try self.managedObjectContext.executeFetchRequest(fetchRequest)
if self.allFruits.count == 0{
print("No saved Fruits")
}
}
catch
{
let fetchError = error as NSError
print(fetchError)
}
}
Then the table view is populated with this specific fruit data. I have got this method for deletion of the Fruits from the table
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if (editingStyle == UITableViewCellEditingStyle.Delete) {
// handle delete (by removing the data from your array and updating the tableview)
managedObjectContext.deleteObject(allFruits[indexPath.row] as! NSManagedObject)
// Attempt to save the object
do{
try appDelegate.managedObjectContext.save()
}
catch let error{
print("Could not save Deletion \(error)")
}
// Remove the deleted item from the table view
self.tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
// Reload the fruits
self.loadFruits()
// Reload the table view
tableView.reloadData()
}
}
This instead just crashes the app every time I try to delete the Fruits.
'NSInternalInconsistencyException', reason: 'Invalid update: invalid
number of rows in section 0. The number of rows contained in an
existing section after the update (5) must be equal to the number of
rows contained in that section before the update (5), plus or minus
the number of rows inserted or deleted from that section (0 inserted,
1 deleted) and plus or minus the number of rows moved into or out of
that section (0 moved in, 0 moved out).'
I am suspecting that there is some issue with the fact that I am using an NSArray as opposed to an NSMutableArray.
How can I fix this issue?
Thanks in advance.
First of all use a Swift array rather than a Foundation array
var allFruits = [NSManagedObject]()
This avoids a lot of type casting.
To keep Core Data and the table view in sync you have to delete the object in Core Data and in the model. The solution to completely reload both the model and the view is very expensive and not needed at all.
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
let objectToDelete = allFruits[indexPath.row]
allFruits.removeAtIndex(indexPath.row)
managedObjectContext.deleteObject(objectToDelete)
//Attempt to save the object
do{
try appDelegate.managedObjectContext.save()
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
}
catch let error{
print("Could not save Deletion \(error)")
}
}
}
deleteRowsAtIndexPaths includes updating the UI so reloading the table view is not needed.
If the model of the table view is NSManagedObject it's recommended to use NSFetchedResultsController
You should update data source before delete. Like this:
//Reload the fruits
self.loadFruits()
//Remove the deleted item from the table view
self.tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
And if you're sure there's no other changes,it's no need to reload data because this costs a lot.
I fixed my code by simply changing this method:
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if (editingStyle == UITableViewCellEditingStyle.Delete) {
// handle delete (by removing the data from your array and updating the tableview)
managedObjectContext.deleteObject(allFruits[indexPath.row] as! NSManagedObject)
//Attempt to save the object
do{
try appDelegate.managedObjectContext.save()
}
catch let error{
print("Could not save Deletion \(error)")
}
//Reload the fruits
self.loadFruits()
//Reload the table view
tableView.reloadData()
}
}
This successfully deletes the object from the core data as well as updating the table view.
Updated for Swift 3 Xcode 8 iOS 10
The following code snippet adds a 'swipe to delete' to your Table Row, and also saves the changes to CoreData.
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
if editingStyle == .delete {
context.delete(tasks[indexPath.row] as NSManagedObject)
//objects.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
//Save the object
do{
try (UIApplication.shared.delegate as! AppDelegate).saveContext()
}
catch let error{
print("Cannot Save: Reason: \(error)")
}
//Reload your Table Data
getData()
//Reload the table view
tableView.reloadData()
} else if editingStyle == .insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view.
}
}

how to delete a cell from a UITableView with multi section in swift

i'm trying to delete a cell from UITableView in swift, i follow this tutorial: http://www.ioscreator.com/tutorials/delete-rows-table-view-ios8-swift
the problem is my UITableView has many section, so i can't delete the cell the way like the tutorial.
any one know how to delete cell form table with multiple section?
thanks.
You cannot delete multiple cells at once with the method described in the tutorial. That will only work for single cell. If you select multiple cells and use button, for example, to trigger delete action, your code could look something like this:
if let indexPaths = tableView.indexPathsForSelectedRows() as? [NSIndexPath] {
for indexPath in indexPaths {
// one by one remove items from your datasource
}
tableView.deleteRowsAtIndexPaths(indexPaths, withRowAnimation: .Automatic)
}
Instead of using numbers[row] in the example you can use numbers[section][row]. So the code will look like:
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return numbers[section].count
}
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == UITableViewCellEditingStyle.Delete {
numbers[indexPath.section].removeAtIndex(indexPath.row)
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
}
}
Neither of answers worked for me. Swift Array indexes are updated upon removal hence for-in loop for indexes from .indexPathsForSelectedRows() provided unexpected results i.e. wrong data/tables removed and eventually crash with index outside of array bounds error. Found good (but really outdated) Objective-C iOS Developer Library example. But it utilised NSMutableArray removeObjectsAtIndexes method, not present with Swift Array. Anyway a good deal of useful tricks in there so worth take a look.
The method which work for me is part from that example but instead of removeObjectsAtIndexes do-while is used to remove rows one by one until all selected rows are removed. The method below called by UIAlertAction similar to Apple example.
func deleteSelectedRows() {
// Unwrap indexPaths to check if rows are selected
if let _ = tableView.indexPathsForSelectedRows() {
// Do while all selected rows are deleted
do {
if let indexPath = tableView.indexPathForSelectedRow(){
//remove from table view data source and table view
self.dataSource.removeAtIndex(indexPath.row)
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
}
} while tableView.indexPathsForSelectedRows() != nil
}else{
// Delete everything, delete the objects from data model.
self.dataSource.removeAll(keepCapacity: false)
// Tell the tableView that we deleted the objects.
// Because we are deleting all the rows, just reload the current table section
self.tableView.reloadSections(NSIndexSet(index: 0), withRowAnimation: .Automatic)
}
// Exit editing mode after the deletion.
self.tableView.setEditing(false, animated: true)
}
Edit: While do-while did it trick for small example I've been working with (jus starting with swift) It's not efficient. Either extending Array or make data source Equatable and use find() or .filter is preferable.But I'm sure there should be a simpler way. The one I'm using now is described on link below:
http://www.rockhoppertech.com/blog/swift-remove-array-item/
func == (lhs: myDataSource, rhs: myDataSource) -> Bool {
if lhs.data == rhs.data &&
lhs.otherData == rhs.otherData {
return true
}
return false
}
struct myDataSource: Equatable {
let data: String
let otherData: String
}
And then:
if let selectedRows = tableView.indexPathsForSelectedRows(){
var objectsToDelete = [myDataSource]()
for selectedRow in selectedRows {
objectsToDelete.append(myDataSource[selectedRow.row])
}
for object in objectsToDelete {
if let index = find(myDataSource, object){
myDataSource.removeAtIndex(index)
}
}
}
tableView.deleteRowsAtIndexPaths([selectedRows], withRowAnimation: .Automatic)
try to this. this works fine.
But don't forget to this before.
func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool
{
return true
}
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath)
{
if editingStyle == .Delete
{
arrayOfnames.removeAtIndex(indexPath.row)
self.tableViewww.reloadData()
}
}

Resources