Animate deleted row with fetched results controller - ios

I’m looking to animate the removal of an item from my table view. I’m using the fetched results controller with CoreData. The issue I’m having is an exception getting raised about how the number of rows must match the number of items.
Here’s the error i get when I delete:
reason: 'Invalid update: invalid number of rows in section 0. The
number of rows contained in an existing section after the update (4)
must be equal to the number of rows contained in that section before
the update (3), plus or minus the number of rows inserted or deleted
from that section (0 inserted, 0 deleted) and plus or minus the number
of rows moved into or out of that section (0 moved in, 0 moved out).'
And here’s the code:
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
let context = appDelegate.persistentContainer.viewContext
if editingStyle == .delete {
// Delete the row from the data source
context.delete(fetchedResultsController.object(at: indexPath))
do {
try context.save()
// Animate deleted row
tableView.deleteRows(at: [indexPath], with: .fade)
} catch {
print("There was an error: \(error)")
}
}
}
Thank you.

You need to also delete the item from your array (or whatever the data source is).
Basically, tableView(_:numberOfRowsInSection:) has to return one less object (or however many items you deleted) when you animate deletion in your UITableView

The problems is that you are removing the row when you delete it and then you are removing it again when the NSFetchedResultsControllerDelegate is telling you do. You must only remove the row once. There are two possible solutions.
1) Remove tableView.deleteRows
The fetchedResultsController delegate will inform your viewController when the object is deleted from the context. So simply delete the object from core data and then trust that the content will be updated appropriately. If you are saving things in the background you may notice a slight delay.
2) Ignore the delegate callback
Set a bool right before deleting the object to ignore the fetchedResultsController callbacks and unset it right after you save. Apple suggest this approach in their documentation.
I personally use the first approach I find it simplest. I am also uncomfortable with ignoring fetchedResultsController updates because if they become out of sync the application can crash.

Related

NSFetchedResultsController - User Driven Changes

Overview
I have an NSFetchedResultsController
The user would be able to add new records (table view in edit mode)
When user taps on add button, I am able to detect the event and I create a new Car (subclass of NSManagedObject that matches the NSFetchedResultsController's predicate)
Question:
How to insert a new row in the table view when the action is user initiated ?
Based on my current implementation, app crashes. Crash message is below.
How to detect exactly when the model changes take effect ? (Based on the crash message I feel I am inserting the row too early)
Note:
I do understand model changes are detected by NSFetchedResultsControllerDelegate but the problem is model is updated and I need the table view to match it.
Normally NSFetchedResultsControllerDelegate detects model changes and I can update using the delegate methods.
My question is, since user adds row, the model is updated first, then the table view must adjust according to that.
Refer: https://developer.apple.com/documentation/coredata/nsfetchedresultscontrollerdelegate
Creation of NSFetchedResultsController:
let fetchRequest : NSFetchRequest<Car> = Car.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "color = %#", argumentArray: ["green"])
let orderIDSortDescriptor = NSSortDescriptor(keyPath: \Car.price, ascending: true)
fetchRequest.sortDescriptors = [orderIDSortDescriptor]
fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: context,
sectionNameKeyPath: nil,
cacheName: nil)
Editing Style
override func tableView(_ tableView: UITableView,
editingStyleForRowAt indexPath: IndexPath) -> UITableViewCellEditingStyle {
let newCarIndex = fetchedResultsController?.fetchedObjects?.count ?? 0
let editingStyle : UITableViewCellEditingStyle
switch indexPath.row {
case newCarIndex:
editingStyle = .insert
default:
break
}
return editingStyle
}
Commit User actions
override func tableView(_ tableView: UITableView,
commit editingStyle: UITableViewCellEditingStyle,
forRowAt indexPath: IndexPath) {
switch editingStyle {
case .insert:
createGreenCar(at: indexPath) //Creating a new Car with color = Green
tableView.insertRows(at: [indexPath], with: .automatic) //This causes the app to crash
default:
break
}
}
Crash Error message:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 1. 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 (1 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'
Thanks to #Jake and #pbasdf, their suggestions helped my identify and rectify the problem.
I am answering for completeness.
Root Cause:
I had multiple sections in my table view and I was inserting row into the wrong section. As a result the table view row count in the relevant section wasn't changing when the model had changed.
Approach User driven updates:
1. Using Arrays
I feel it is better to transform the results into an array and use the array as the data source instead of the NSFetchedResultsController for user driven updates.
2. Using NSFetchedResultsController:
When user inserts / deletes / moves rows UITableViewDataSource methods are invoked:
When user inserts / deletes row tableView(_:commit:forRowAt:) will be invoked
When user moves row tableView(_:moveRowAt:to:) would be invoked
Update Core data accordingly for the above methods
Updating core data will cause NSFetchedResultsControllerDelegate to be invoked
In controller(_:didChange:at:for:newIndexPath:) do the following:
For insert - add indexPaths
For delete - remove indexPaths
For move - do nothing as the UI is already up to date (User has moved the rows), later in controllerDidChangeContent(_:) invoke tableView.reloadData() after a delay of 0.5 seconds.
Note:
When the user moved the row on iOS 11.2 (using NSFetchedResultsController) I did encounter the following warning:
UITableView internal inconsistency: _visibleRows and _visibleCells must be of same length. _visibleRows
I didn't know how to resolve it, so sticking with the array implementation for now.

tableView.deleteRows crashes while trying to delete last row of tableView

So i have a tableViewController which does basic stuff such as listing, deleting and editing. Basically, that tableViewController is listing stuff from a core data, using an auxiliary dictionary to get all the objects and then list them. Everything works fine. I can list all the objects, select them, load them, all fine and with the right values. Except, when i try to delete a middle row and then delete the last row of this tableViewController, it crashes, and I'm mostly certain it crashes on tableView.deleteRows(at: [indexPath], with: .fade).
NOTE: if i delete all the items by order, from the last to the first, it doesn't crash
I've looked for similar solutions but all solutions metion that numberOfRowsInSection should keep an atual value of the rows existing, which I have, using the auxiliary dictionary .count, mentioned before (In code below)
This is the error i get (and once again, i already looked it up, and I always reach the same solution, which I think it's not what is happening in my case):
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 (4) must be equal to the number of rows contained in that section before the update (4), 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).'
Some pieces of code:
Definition of my dictionary, called lists:
var lists = [Int : [Product]]()
Then the definition of number of rows:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return lists.count
}
And then, where it crashes:
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
lists.removeValue(forKey: indexPath.row) //Deletes the entry on dictionary
deleteProductWithId(row: indexPath.row) //Deletes from the core-data
tableView.deleteRows(at: [indexPath], with: .fade) //Crashes here
//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
}
}
Anyone has any idea what I could be doing wrong?
Thanks in advance
Firstly, don't use dictionary for Tableview as dictionary is unordered while tableview needs ordered data. Second is your dictionary's count is still 4 because lists.removeValue(forKey: indexPath.row) will only set value to nil for that key instead of removing whole keypair. You need to delete whole keypair for this case
User array in this form
var lists = [[Product]]()
and then in cellfor row simply:
lists[indexPath.row]
and for deleting row:
lists.remove(at:indexPath.row)
NOTE: if i delete all the items by order, from the last to the first, it doesn't crash
That's because when you don't delete them in order, the index paths of the cells below the row you just deleted change, but your dictionary keys are not updated to reflect that.
Solution: don't cache index paths (index paths' rows in your case). They are bound to change. Don't mix model identifiers and view identifiers.

NSInternalInconsistencyException occurs when insertng indexPath into tableView but only when row count gets above a certain threshold

I have two tables in which I am dragging and dropping between them: sourceTableView and targetTableView. They each have their own tableViewController. A parentContainerViewController manages a 'slide out' UI which allows the user to 'longPressGesture' on the sourceTableView cell which causes sourceTableView to slide out exposing the targetTableView below at which time the user can 'drop' the dragged cell into place within the targetTableView. All works well as it should until the targetTableView grows to about 21-27 rows in size. Once we get to this size and if I re-run the project I start to get the NSInternalInconsistencyException. The detail of the error reads: reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (27) must be equal to the number of rows contained in that section before the update (0), plus or minus the number of rows inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'
At first I thought I was getting a race condition issue since the number of rows before the update is "0" and after it is "27". Maybe the tableView wasn't able to load before the user drags in the item and calls the insertIndex method on tableView. But then I noticed that it works fine until we get to a certain length. Race condition wouldn't make sense in this case. Now I am thinking it has something to do with with 'visible cell' loaded vs not loaded. But why would it be '0' rows? I have looked all over for the answer. There are many NSInconsistency but none that seem to answer my specific problem.
This is the function call to tell targetTableViewController to insert an item into its datasource then tells targetTableView to insertRowsAtIndexPath :
dragDropDataSource.tableView(self, insertDataItem: item, atIndexPath: indexPath)
self.beginUpdates()
self.insertRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Fade)
self.endUpdates()
Here is the targetTableViewController code implementing the above call:
func tableView(tableView: UITableView, insertDataItem dataItem : AnyObject, atIndexPath indexPath: NSIndexPath) -> Void {
if let di = dataItem as? DataItem {
data.insert(di, atIndex: indexPath.row)
}
data is the array that stores the 'DataItems' which are shown in the tableView rows.
Additionally. The targetTableView is initialized with data via a call in its viewDidLoad() to getMyTopTen()
func getMyTopTen() {
let pwAPI = PowWowAPI()
pwAPI.getMyTopTen(self.boardId!) {
(entries:[Entry])in
for(var i=0; i < entries.count; i++) {
let dataItem = DataItem(indexes: String(i), entry: entries[i])
self.data.append(dataItem)
}
}
dispatch_async(dispatch_get_main_queue()) {
self.targetTableView.reloadData()
}
}
I hope I have explained the problem sufficiently. If not please help me make this question better. I am new to asking questions here.

Deleting UITableView Room error

I am getting the follow error when trying to delete a TableView item.
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 (4) must be equal to the number of
rows contained in that section before the update (4), 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 using the following code:
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
let roomToDelete : House = self.frc.objectAtIndexPath(indexPath) as! House
moc.deleteObject(roomToDelete)
do {
try moc.save()
} catch {
print("Failed to save upon delete")
return
}
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
self.tableView.reloadData()
}
}
If I leave the last two lines of code off then I do not get the error, BUT the line does not delete because I am only removing the data from Core Data. If I leave the table and return the line is gone. I assume the error is because I am somehow not linking the line removal with the number of rows in section so there is a mismatch. All of the searches I have done come back with similar programming to what I have.
You should be setting your view controller as the FRC delegate and using the delegate methods to update the table view. As the FRC is your data source it must know about the deletion in order to update the row count.
In your commit method you should just be deleting the object from the context. You should not delete the row from the table or reload the table (that should be done by the FRC delegate methods).
I was able to get it to work by -
1) Removing
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
from the commit editing style function AND
2) Adding
self.tableView.reloadData()
to the end of the viewDidLoad function

iOS Swift 2 - Error deleting tableView row

I've an error when I try to delete a row from a tableView.
The code goes like the following
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == UITableViewCellEditingStyle.Delete {
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
tableView.reloadData()
}
}
The error printed from Xcode is the following:
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).
I've read here on Stack this error but I can't figure it out.
:(
Thanks in advance.
You deleted the row from the table, but first you need to delete the corresponding datum from your data model. For example, if your data model is an array, then you might call removeAtIndex on it to remove that row. You must always proceed in this order: adjust the data model, adjust the table view.
(There is no need to reload the data at this time; you can omit that line.)

Resources