Core Data Table View Deleting Crash - objective-c

I have a parent-child Core Data relationship set up in my iPhone app. I have a Manufacturer object and a Car object. It is a to-many relationship with the Manufacturer being the owner. The main view is a Table View containing the Manufacturers. The detail view is another Table View with the different types of cars. I have been using Tim Roadley's Core Data Tutorial as the base. This tutorial uses Stanford's Core Data Table View Library for the table views.
Adding Cars and Manufacturers gives me no problem, but when I go in and delete with multiple cars in the table view I get this error:
*** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-1914.84/UITableView.m:1037
2012-07-29 23:39:33.561 App [16368:c07] * 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 (0) must be equal to the number of rows contained in that section before the update (2), 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).'
If I delete the only car, it works fine until I try to add a new car, when I get this error:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Illegal attempt to establish a relationship 'manufacturer' between objects in different contexts (source = <Car: 0x6d96da0> (entity: Car; id: 0x6d8a3c0 <x-coredata:///Car/tC78E17EB-1D68-4998-8C4D-6D1199CE253F4> ; data: {
dateAdded = nil;
manufacturer = nil;
carName = new;
}) , destination = <Manufacturer: 0x6bb1f40> (entity: Manufacturer; id: 0x6d87340 <x-coredata://2E8DDF34-B01A-4203-A53E-73DBE6A2F976/Garden/p6> ; data: <fault>))'
Here is my editing method:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete) {
[self.tableView beginUpdates];
Plant *plantToDelete = [self.fetchedResultsController objectAtIndexPath:indexPath];
NSLog(#"Deleting plant '%#'", plantToDelete.plantName);
[self.managedObjectContext deleteObject:plantToDelete];
[self.managedObjectContext save:nil];
//delete empty tableview row
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationRight];
NSLog(#"Before performFetch...");
[self performFetch];
NSLog(#"After performFetch...");
[self.tableView endUpdates];
}
}
The performFetch method is contained in the previously mentioned CoreDataTableViewController files. For your convenience, here it is:
(void)performFetch
{
_debug = YES;
if (self.fetchedResultsController) {
if (self.fetchedResultsController.fetchRequest.predicate) {
if (self.debug) NSLog(#"[%# %#] fetching %# with predicate: %#", NSStringFromClass([self class]), NSStringFromSelector(_cmd), self.fetchedResultsController.fetchRequest.entityName, self.fetchedResultsController.fetchRequest.predicate);
} else {
if (self.debug) NSLog(#"[%# %#] fetching all %# (i.e., no predicate)", NSStringFromClass([self class]), NSStringFromSelector(_cmd), self.fetchedResultsController.fetchRequest.entityName);
}
NSError *error;
[self.fetchedResultsController performFetch:&error];
if (error) NSLog(#"[%# %#] %# (%#)", NSStringFromClass([self class]), NSStringFromSelector(_cmd), [error localizedDescription], [error localizedFailureReason]);
} else {
if (self.debug) NSLog(#"[%# %#] no NSFetchedResultsController (yet?)", NSStringFromClass([self class]), NSStringFromSelector(_cmd));
}
[self.tableView reloadData];
}
According to other questions, I am doing this correctly by using beginUpdates and endUpdates. This is a puzzling error. Thanks for your help.

I am not sure why you are performing the fetch again, if an object is removed from the context, the fetched results controller is aware of that change already. I think the main problem you have is calling perform fetch in the middle of processing updates to the table. If you comment that line out, does it still have the error?
Additionally, the following may or may not be another part of the the problem as this is where you are differing from my own code:
I have not seen the begin/end edits calls in tableView:CommitEditingStyle: before. My own process in that method generally deletes the object without any concern for the table row. The table rows are reconciled in the fetchedResultController delegate methods like so:
-(void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
//the fetch controller is about to start sending change notifications so prepare the tableview
[self.tableView beginUpdates];
}
-(void)controller:(NSFetchedResultsController *)controller
didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath {
// reconcile your rows here
switch(type) {
case NSFetchedResultsChangeInsert:
break;
case NSFetchedResultsChangeDelete:
// this one is you
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationRight];
break;
case NSFetchedResultsChangeUpdate:
break;
case NSFetchedResultsChangeMove:
break;
}
-(void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
// The fetch controller has sent all current change notifications, so tell the table view to process all updates.
[self.tableView endUpdates];
}
As long as the number of rows matches the number of fetched objects after all that, you should not have that error.

Try removing the lines
//delete empty tableview row
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationRight];
NSLog(#"Before performFetch...");
[self performFetch];
NSLog(#"After performFetch...");
I believe that CoreDataTableViewController will automatically handle removing the row from the table. You are essentially removing the row twice which is causing the error.

I was having the same error occur when I would open a tableViewController that was a subclass of Tim Roadley's CoreDataTableViewController. My specific app does not require the user to be able to add or delete rows, but it allows them to reorder the fetched results by name and by distance as well as search the data. I used Dean David's answer (the accepted answer above), but after every case statement I only added a break statement. So far that has worked for this app!

Related

How to perform the batch updates on table View in ios 10 and below in objective c

I have been using the core data for chat application and updating the chat table in delegate methods of NSfetchResultController controllerDidChangeContent
NSMutableArray<NSOperation *> *blockOpetations;
blockOpetations = [NSMutableArray new];
-(void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
[self.chatTable beginUpdates];
}
-(void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
switch (type) {
case NSFetchedResultsChangeInsert:{
[blockOpetations addObject:[NSBlockOperation blockOperationWithBlock: ^{
[self.chatTable insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationBottom];
}]];
}
break;
default:
break;
}
}
-(void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
if (#available(iOS 11.0, *))
{
[self.chatTable performBatchUpdates:^
{
for(NSOperation *operation in blockOpetations)
{
[operation start];
}
} completion:^(BOOL finished)
{
NSInteger lastItem = [self.fetchedResultsController sections]
[0].numberOfObjects - 1;
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:lastItem
inSection:0];
[self.chatTable scrollToRowAtIndexPath:indexPath
atScrollPosition:UITableViewScrollPositionBottom
animated:YES];
}];
} else {
}
[self.chatTable endUpdates];
}
I am performing the batch updates on table view for iOS 11 and higher.
I want to perform the batch updates on table in iOS 10 and lower version , since batchupdate is not available below iOS 10.
I want to to know how to perform such thing in lower version.
I have taken the "blockOperation" an array of NSOperation , because if multiple message comes at the same time so table view updates the newly inserted rows serially without crash.
If I may have not taken the BlockOperation then some time the error comes
:
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 (20) must be equal to the number of rows contained in that section before the update (10), 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).
The code written under method named "ControllerDidChangeContent", want to implement same task in iOS 10 and lower
Before iOS 11, this is done with
self.chatTable.beginUpdates()
// do stuff here
self.chatTable.endUpdates()

Assertion failure when trying to delete a row from UITableView

I got an assertion failure when I try to delete a row from a UITableView using UITableViewCellEditingStyleDelete. Hope someone could tell me what is going wrong in my code.
*** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-2372/UITableView.m:1070
2013-01-30 14:19:21.450 MyApp[48313:c07] *** 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 (3), 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).'
*** First throw call stack:
(0x28ee012 0x1d3fe7e 0x28ede78 0x19fef35 0xf51b8d 0xf5df95 0xf5dfc3 0xcfe9 0xf6c384 0x10a9b1b 0x1d53705 0xeb3920 0xeb38b8 0xf74671 0xf74bcf 0xf746a6 0x1144f95 0x1d53705 0xeb3920 0xeb38b8 0xf74671 0xf74bcf 0xf73d38 0xee333f 0xee3552 0xec13aa 0xeb2cf8 0x2d1adf9 0x2d1aad0 0x2863bf5 0x2863962 0x2894bb6 0x2893f44 0x2893e1b 0x2d197e3 0x2d19668 0xeb065c 0x24fd 0x2435)
libc++abi.dylib: terminate called throwing an exception
(lldb)
Here is my code:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [records count];
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete)
{
// Delete record from the sqlite database
NSNumber *recordDBID = [records objectAtIndex:indexPath.row];
Record *myRecord = [[Record alloc] initWithDBID:recordDBID database:UIAppDelegate.formManager.connection];
BOOL valueX = [myRecord deleteRecordInDb:recordDBID];
[myRecord release];
// Delete record from array
NSMutableArray *aNewArray = [[NSMutableArray alloc] initWithArray:records];
[aNewArray removeObjectAtIndex:indexPath.row];
records = aNewArray;
[aNewArray release];
// Delete the record from the table
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView endUpdates];
[tableView reloadData];
}
I think there's a chance that you may be overcomplicating the removal of the table view row.
You do not need to call [tableView beginUpdates] and [tableView endUpdates] if you are only going to be removing a single row between those two calls, so you can lose them. Begin and end updates are only required if you are going to be carrying out multiple insert or delete actions at the same time.
Secondly, calling [tableView reloadData] at this point is overkill as the table view will automatically request the information it needs as part of the delete action using the assigned delegate/datasource. You can reduce the last part of the code to this:
// Delete the record from the table
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
It may be that the current row removal code (in combination with the wider code base) is confusing the system.
If the above does not help we'll need to see all code where the records array is being modified. In this event I'll update my answer.
In my case I have typos: it was indexPath instead newIndexPath in insertRowsAtIndexPaths
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?)
{
switch type
{
case .Insert:
self.tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Bottom)
case .Delete:
self.tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .None)
case .Update:
self.tableView.reloadRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
case .Move:
self.tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .None)
self.tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Bottom)
}
}

NSRangeException when using Core Data Asynchronous UISearchDisplayController

I'm asynchronously fetching data, and I've used this as a guide: http://deeperdesign.wordpress.com/2011/05/30/cancellable-asynchronous-searching-with-uisearchdisplaycontroller/
In
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString{
//setup request / predicate etc...
[self.searchQueue addOperationWithBlock:^{
NSError *error;
self.matchingObjects = [self.managedObjectContext executeFetchRequest:request error:&error];
[request release];
[[NSOperationQueue mainQueue] addOperationWithBlock:^
{
[self.searchDisplayController.searchResultsTableView reloadData];
}];
}];
// Return YES to cause the search result table view to be reloaded.
return NO;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
// Return the number of rows in the section.
return [self.matchingObjects count];
}
Every now and then I'll get something to the effect of:
*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[NSArray objectAtIndex:]: index 0 beyond bounds for empty array'
This is thrown at the matchingObjects ivar when accessing it to construct the table cell in:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
The crash doesn't occur all the time, just seems to happen on random occasions. I'm guessing that somewhere the count on the matchingObjects array is returning a certain value, which changes and is not being updated.
I'm not entirely sure of how to deal with this - been looking over this for hours, is there something I'm missing?
I figured out what it was - took me a while, but I looked again at the example that I just linked. I was updating the self.matchingObjects iVar in the background thread, which on some occasions caused a mismatch between the range of the array available in the main thread and the background thread. So for example, the variable may have been updated in the background thread, and the main thread may still be accessing a part of the range that no longer exists in the variable since it was updated.
Fixed it by amending my code as follows:
[self.searchQueue addOperationWithBlock:^
{
NSError *error;
NSArray *results = [self.managedObjectContext executeFetchRequest:request error:&error];
[request release];
[[NSOperationQueue mainQueue] addOperationWithBlock:^
{
self.matchingObjects = results;
[self.searchDisplayController.searchResultsTableView reloadData];
}];
}];
Now the results of the search are loaded into a temporary holding array named "results", and the matchingObjects iVar is first updated in the main thread and then the tableView is reloaded. This way, the tableView always is referring to an array that is never changed whilst it is being accessed, since the tableView relies on matchingObjects to get the number of rows and data.

How can I refresh a NSFetchedResultsController?

I have a NSFetchedResultsController which displays data in a UITableView. I'm using the boiler plate code provided by Xcode when choosing to create a Core Data project. I added the following predicate to the NSFetchRequest the NSFetchedResultsController uses (before NSFetchedResultsController is initialized):
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"deleted == NO"];
[fetchRequest setPredicate:predicate];
Now in another location of my app, I set the deleted property like so (pseudo-code):
myManagedObject.deleted = YES
saveDataContext
When I return to the TableViewController, it still shows this "deleted" row.
When I try to reload the table view, nothing happens.
When I try to reload the fetchedResultsController using performFetch, it says:
'FATAL ERROR: The persistent cache of section information does not match the current configuration. You have illegally mutated the NSFetchedResultsController's fetch request, its predicate, or its sort descriptor without either disabling caching or using +deleteCacheWithName:'
If I remove the caching, in the init method, call performFetch, then call [myTable reloadData] it works.
Isn't there a simpler way to refresh the data? Preferably one that allows you to use the caching feature of NSFetchedResultsController?
As far as I know, the only place I am modifying the fetch request, predicate, or sort descriptor is in the same method that allocs and inits the NSFetchedResultsController, so it seems that the error message that it displays is incorrect.
Update: Now that I understand the NSFetchedResultsController a bit better, I understand that it won't remove rows for you automatically, and it's the controller:didChangeObject:atIndexPath:forChangeType:nowIndexPath: method that is primarily responsible for deleting rows. I had this method implemented, since I used Apple's template for my project.
However, in my case I'm not actually deleting an item, I'm just updating a property (deleted) to specify that the item should no longer exist in the list. This means that the controller:didChangeObject:atIndexPath:forChangeType:nowIndexPath: method's change type is NSFetchedResultsChangeUpdate and not NSFetchedResultsChangeDelete. I updated the code to something such as:
case NSFetchedResultsChangeUpdate: {
MyObj *obj = (MyObj *)anObject;
if (obj.deletedValue) { // NOTE: deletedValue returns the deleted property as BOOL
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
} else {
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
}
}
The problem with this is that I get the error message:
Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (3) 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 (1 inserted, 2 deleted). with userInfo (null)
Basically it's complaining that the number of objects in the NSFetchedResultsController and the number of cells in the table aren't synced.
Hopefully this makes it clearer:
// DB Items (Name, Deleted):
[Foo, NO]
[Bar, NO]
[Baz, NO]
// mark one as deleted
Objects[1].deletedValue = YES;
// DB items now look like so:
[Foo, NO]
[Bar, YES]
[Baz, NO]
Even though NSFetchedResultsController's NSFetchRequest's NSPredicate is set to deleted == NO, the NSFetchedResultsController still sees 3 items instead of 2.
How can I solve this issue? Would I need to refresh the NSFetchedReultsController somehow? What else could be the problem?
The problem was that I didn't realize that deleted is a reserved word in this case. I had to rename the field and it worked fine.
You need to setup a delegate to the NSFetchedRequestController in order to keep track of changes. To handle batch updates you need to override three methods:
controllerWillChangeContent: - (Setup lists of index paths here)
controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: - (Store for each call here)
controllerDidChangeContent: (Excute updates to UITableView here.)
For the much simpler case where you know that only rows will always be updated one at a time, then go for just something like this:
-(void)controller:(NSFetchedResultsController *)controller
didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath;
{
switch (type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationTop];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationTop];
break;
case NSFetchedResultsChangeUpdate:
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
It seems pretty clear to me, from the error message, that an NSFetchedResultsController does not expect you to change its fetch request once it is created.
What you need to do is add the predicate to the fetch request before the NSFetchedRequestController is initialised. If you want to change the predicate of the fetch request while the app is running, you probably need to create a new NSFetchedRequestController and replace the old one completely.

NSManagedObjectContext: autoupdate or not?

I need to understand something about NSManagedObjectContext update.
I have a UISplitView with a UITableViewController on the RootView and a UIViewController on the Detail View. When I tap in a row with data, I load some data into labels and a UITextView where I can update that field:
- (void)textViewDidEndEditing:(UITextView *)textView {
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
[[listOfAdventures objectAtIndex:indexPath.row] setAdventureDescription:textView.text];
}
Ok. This works correctly, the description is updated.
Also, someone might wants to delete a row:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"playerPlaysAdventure.adventureName==%#",[[listOfAdventures objectAtIndex:indexPath.row] adventureName]];
NSArray *results = [[AdventureFetcher sharedInstance] fetchManagedObjectsForEntity:#"Player" withPredicate:predicate withDescriptor:#"playerName"];
[moc deleteObject:[listOfAdventures objectAtIndex:indexPath.row]];
for ( Player *player in results ) {
[moc deleteObject:player];
}
[listOfAdventures removeObjectAtIndex:indexPath.row];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:YES];
[self clearDetailViewContent];
NSError *error = nil;
if ( ![moc save:&error] ) {
NSLog( #"Errore nella cancellazione del contesto!" );
abort();
}
}
else if (editingStyle == UITableViewCellEditingStyleInsert) {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
}
}
So here's my problem: if I comment the rows about the saving of my MOC, the adventure is only momentarily deleted. If you quit the app and the reopen it, the object is still there. This doesn't happen with the update of a field. I'd like to know why and if I should save moc also in textViewDidFinishEditing method.
Thank you in advance.
It's the difference between changing an attribute of an object and adding or removing an entire object in the object graph.
In the first block, you change an attribute of an existing object which saves automatically unless you run an undo. This is because the object already exist in the object graph and no other objects have to be altered to make the change.
In the second block, you are removing an entire object and potentially altering the object graph itself by changing the relationships between objects. That change will not be committed until an implicit save because potentially it can trigger a cascade of changes throughout a large number of objects.