I hava a UINavigationController. The first level is a UITableViewController, the second level just shows details on one of the items of the table view.
In this detail view, I can delete the item. It deletes the underlying managed object.
When I pop back to the view, I have a crash. I understand why, it's because I didn't update the cached array that contains the data.
I looked at several tutorials and I don't exactly understand how am I supposed to handle deletion. Maybe I don't understand exactly where I should fetch the objects in the model. Should I do a query for every cellForRowAtIndexPath and take the item in the result at position indexPath.row? It doesn't look efficient. Should I check for changes somewhere and recache the whole query in an array. I would think CoreData would provide something more natural but I couldn't find it so far.
Thanks in advance.
It is fairly simple. In the child view you should (really, really should) have a reference to the NSManagedObject you are working with. When you want to delete it then you just:
NSManagedObjectContext *moc = [[self myObject] managedObjectContext];
[moc deleteObject:[self myObject]];
NSError *error = nil;
if (![moc save:&error]) {
NSLog(#"Save failed: %#\n%#", [error localizedDescription], [error userInfo]);
}
This will delete the object. The parent, since it is using a NSFetchedResultsController (which you should also REALLY be doing) will take care of itself.
It seems somewhat non-standard deleting an item in the parent controller from your detail controller, but perhaps it makes sense in your case. I presume you know that you can directly delete items in the tableview. There are many example code projects from Apple which along with the docs should give you an idea how to do that.
To answer your question, you could create a property/variable in your detail controller's class which holds a reference to the tableview controller then send a message to that controller to handle the delete. Creating a protocol for this would be good style but not necessary. When the tableview class receives the delete item message, it updates the array, and when that view is redisplayed you should call reloadData on the tableview. This is the standard paradigm: make changes to your underlying data model and tell the tablview to reload.
In case you don't use NSFetchedResultsController, all you need to do is implement the following method:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
// Delete the managed object at the given index path.
NSManagedObject *rowToDelete = [currentRows objectAtIndex:indexPath.row];
[managedObjectContext deleteObject:rowToDelete];
// Commit the change.
NSError *error;
if (![managedObjectContext save:&error]) {
// Handle the error.
NSLog(#"Failed to save to data store: %#", [error localizedDescription]);
NSArray* detailedErrors = [[error userInfo] objectForKey:NSDetailedErrorsKey];
if(detailedErrors != nil && [detailedErrors count] > 0) {
for(NSError* detailedError in detailedErrors) {
NSLog(#" DetailedError: %#", [detailedError userInfo]);
}
}
else {
NSLog(#" %#", [error userInfo]);
}
}
// Update the array and table view.
[currentRows removeObjectAtIndex:indexPath.row];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:YES];
}
}
currentRows is a NSArray of the objects you display in the table.
Cheers
Related
so I'm having a Core Data & iCloud combined Database that is working fine for me.
Some users though had a certain crash after trying to delete NSManagedObject's that are respresented in an UITableView. In the debug-logs that I attach to the crashreports there is the following error-log before the crash:
2013-05-08 22:38:51.851 MyApp[11819:907] Unresolved error Error Domain=NSCocoaErrorDomain Code=134030 "The operation couldn’t be completed. (Cocoa error 134030.)", {
}
Unfortunately the info about the error: {} is empty and I never had the error myself, so I don't know how to recreate it. It did occur at least 21 times for customers though.
(Whats even worse is, once they reopen the App, some NSManagedObject is in an invalid state and my tableview has problems loading it. But that just at the side)
The actual error is "SIGABRT" in " tableView:commitEditingStyle:forRowAtIndexPath:", here is the respective code:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
ICLog(#"Deleting the row %i,%i from the data source", indexPath.row, indexPath.section);
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
[context deleteObject:[self.fetchedResultsController objectAtIndexPath:indexPath]];
//Save the context
NSError *error;
if (![context save:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
}
}
PS: From the Logs its also clear that this is all run from the main thread.
Anyone here who had this problem too?
UPDATE:
Luckily I had the same error in another App I'm working on just now, in a device I have access too (so far I had never saw this happening on any of my devices), so now I can investigate it while it happens. This is what I found out:
a) it still occurs after switching off icloud in the settings
b) the error occurs everytime I try to delete or insert any NSManagedObject, even if I do it in my Appdelegate right after starting, without ever accessing the data somewhere else
So it seems like something completely stops my databse from being updated. I can catch the above the exception, then the App at least isn't crashing. But of course no changes to the database are saved.
Errors on [context save:] often occur when you try to save an invalid state of your datamodel.
Since you have deleted an object before, i would suspect that this object was in a relation with some other object that still is present, but now has it's relation in an invalid state (nil for a relation that is non-optional)
Checking your delete rules could help, maybe some relation to the object you delete needs to be optional or you have to propagate the deletion correctly.
I will be making some assumptions here.
What I can see is that you delete the CoreData object using the object in fetchedResultsController but you never unset the object from whatever you use as your ViewController iVar fetch storage in numberOfRowsInSection and re-organise your table index after deletion.
Let say your original [indexPath.row count] is 7 then you go on and delete indexPath.row = 5 from your tableView. Then you go on and try to delete another object at indexPath.row = 6 that item exists on your tableView index because you havent re-organised the indexs, but it wont exist in CoreData.... given that now the max count is 6 that the index value is now 5 of what originally was 6.... This could be what your users are experiencing...
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [yourFetchedDataVariable count];
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
ICLog(#"Deleting the row %i,%i from the data source", indexPath.row, indexPath.section);
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
[context deleteObject:[self.fetchedResultsController objectAtIndexPath:indexPath]];
//Save the context
NSError *error;
if (![context save:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
// the possible solution
[yourFetchedDataVariable removeObjectAtIndex:indexPath.row];
// also now you want to re-order your array index...
[yourTable reloadData]; // and re order your table indexses aswell
}
}
The optional relationship are you sure it is optional? and optional in what sense to Xcode? I ahd this issue once and it was due to a "optional" relationship.
I have UITableView with NSFetchedResultsController and a main context attached to it.
I'm updating my main context from background.
- (void)contextDidSave:(NSNotification *)notification
{
NSManagedObjectContext *context=[self managedObjectContext];
[context performSelectorOnMainThread:#selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES];
if ([context hasChanges])
{
[context performSelectorOnMainThread:#selector(save:) withObject:nil waitUntilDone:YES];
}
}
My UITableView reloads with new data and everything is fine, but some UITableView cell's data must be populated from fetched properties and I need that values to be refreshed.
I've read about NSManagedObjectContext's method refreshObject:mergeChanges: that might help. Where should I place it? If I will place it in cellForRowAtIndexPath's helper method - configureCell:atIndexPath: this will result in cycling row updates.
Here's the piece of cell configuration code:
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)theIndexPath
{
//NSLog(#"PTAgencyList: configureCell: atIndexPath: %i %i", theIndexPath.section, theIndexPath.row);
NSManagedObject *managedObj=[self.agenciesFetchedResultsController objectAtIndexPath:theIndexPath];
if (managedObj==nil)
{
NSLog(#"PANIC: fetchedResultsController objectAtIndexPath: returned nil object");
return;
}
else
{
CDAgency *agency=(CDAgency *)managedObj;
[agency city];
NSLog(#"agency.city_server_id: %#", agency.city_server_id);
NSLog(#"agency.city.name: %#", ((CDCities *)[[agency city] objectAtIndex:0]).name);
NSFetchRequest *fr=[[NSFetchRequest alloc] init];
[fr setEntity:[NSEntityDescription entityForName:#"Cities" inManagedObjectContext:self.context]];
[fr setPredicate:[NSPredicate predicateWithFormat:#"server_id=%#", agency.city_server_id]];
NSArray *res=[self.context executeFetchRequest:fr error:nil];
NSLog(#"fetched city name: %#", ((CDCities *)[res objectAtIndex:0]).name);
After context update:
fetched property city is nil (I think it is because it still points to the old city entry that was removed), but if I'm doing the fetch manually I'm getting fresh data.
Just call your fetched property explicitly where you configure your cell.
[managedObject fetchedPropertyAttribute];
It should recompute as expected.
Also, consider using a relationship rather than a fetched property.
I'm having some issues getting Core Data to save new rows that I add when using a UITextField. Here is my method for inserting objects into my table view. What should happen is when I click the add button, a textfield should be added, and then go right into edit mode. Then when the user clicks done on the keyboard the textfield should end editing and then the textfield should save the entry into core data.
Edit: removed call to textFieldDidEndEditing in the insertNewObject:(id)sender method. It was crashing the app
- (void)insertNewObject:(id)sender {
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
TehdaItem *item = [NSEntityDescription insertNewObjectForEntityForName:#"TehdaItem" inManagedObjectContext:context];
// If appropriate, configure the new managed object.
// Normally you should use accessor methods, but using KVC here avoids the need to add a custom class to the template.
// Putting the cell in edit mode
TehdaTableViewCell *editcell;
for (TehdaTableViewCell *cell in [self.tableView visibleCells]) {
if (cell.itemLabel.text == item.itemTitle) {
editcell = cell;
break;
}
}
[editcell.itemLabel becomeFirstResponder];
// The cell needs to call the method textfield did end editing so that it can save the new object into the store
// Save the context.
NSError *error = nil;
if (![context save:&error]) {
// 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.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}
Here is my textFieldDidEndEditing method:
- (void)textFieldDidEndEditing:(UITextField *)textField {
TehdaTableViewCell *cell = (TehdaTableViewCell *) textField.superview.superview;
TehdaItem *item = [self.fetchedResultsController objectAtIndexPath:[self.tableView indexPathForCell:cell]];
//TehdaTableViewCell *cell;
item.itemTitle = cell.itemLabel.text;
}
Not really sure where to go from here. Any help would be appreciated.
Thanks.
You can use
NSError *error;
[item.managedObjectContext save:&error];
if (error) {
// Triage the problem and respond appropriately
}
in the - (void)textFieldDidEndEditing:(UITextField *)textField method. But if I were you I'd do some validation before you save the object.
Is there any method to do this job? I could not find in the document.
To delete an object from database use the following method:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
[managedObjectContext deleteObject:[userResults objectAtIndex:indexPath.row]];
[userResults removeObjectAtIndex:indexPath.row];
// Save the context.
NSError *error = nil;
if (![managedObjectContext save:&error]) {
/*
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. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
[self.tableView reloadData];
}
Where userResults is the NSMutableArray type, which you use to fetch data and store temporary values
If you mean deleting you can use [context deleteObject:object] where context is your NSManagedContext and object is your NSManagedObject derived object.
I am working on RSS reader code where articles get downloaded and are viewable offline.
The problem is only after all articles are downlaoded the tableview containing headlines gets updated. Core data is used. So everytime NSobjectcontext is saved , [self tableview updatebegins ] is called.The table is getting updated via fetchcontroller core data.
I tried saving NSobjectcontext everytime an article is saved but that is not updating the tableview. I want a mechanism similar to instapaper tableview where articles get saved and tableview gets updated immediately. Please help if you know the solution. Thanks in advance.
Adding code for better understanding
AppDelegate.m contains following code
- (void)feedSuccess:(ZSURLConnectionDelegate*)delegate
NSManagedObjectContext *moc = [self managedObjectContext];
CXMLElement *root = [document rootElement];
CXMLElement *channel = [[root elementsForName:#"channel"] lastObject];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:[NSEntityDescription entityForName:#"FeedItem" inManagedObjectContext:moc]];
for (CXMLElement *item in [channel elementsForName:#"item"])
{
// push element in core data and then save context
//Save context
[moc save:&error];
ZAssert(error == nil, #"Error saving context: %#", [error localizedDescription]);
}
this triggers the table change code in RootviewController.m
- (void)controllerWillChangeContent:(NSFetchedResultsController*)controller
{
[[self tableView] beginUpdates];
}
If you use the NSFetchedResults controller, simply use its delegate method
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller;
as follows:
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
// In the simplest, most efficient, case, reload the table view.
[self.tableView reloadData];
}
Every time you update the underlying NSManagedObjectContext, the table will be automatically updated too.