core data fetched property. refresh after update - iphone

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.

Related

Loading core data into array, then populating table view, reordering array's objects doesnt persist

First let me tell you what im trying to do. Load data into array(from core data entity), populate table view,
if user wants, reorder cells and update the array.
Thats it.
I have found my problem, i just dont know how to fix it:
I am loading my Entities data/attributes into an array and populating my tableview with the data
(BELOW BEHOLDS THE PROBLEM):
-(void)viewWillAppear:(BOOL)animated{
if (self.context == nil)
{
self.context = [(RootAppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext];
}
NSFetchRequest *request = [[NSFetchRequest alloc]init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"HandgunAmmo" inManagedObjectContext:self.context];
[request setEntity:entity];
NSError *error;
//PROBLEM!!! the 2 lines below this.
NSMutableArray *array = [[self.context executeFetchRequest:request error:&error] mutableCopy];
[self setTableArray:array];
[self.ammoTable reloadData];
[super viewWillAppear:YES];
}
SO at this point the table view is loaded with data (accounting for cellForRow being called)
The user moves a few cells around, and i update the array as follows:
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath
{
// fetch the object at the row being moved
NSString *r = [self.tableArray objectAtIndex:fromIndexPath.row];
// remove the original from the data structure
[self.tableArray removeObjectAtIndex:fromIndexPath.row];
// insert the object at the target row
[self.tableArray insertObject:r atIndex:toIndexPath.row];
[self.ammoTable reloadData];
}
As you can see the code for reordering the array should work.
But, in the viewWillAppear method, I am loading the entities attributes into the array again and using it to populate the table view which is the problem. When i update the array, its not updating the order of the objects inside of the entity. Does anyone know how to update that? I would really appreciate it!
Thank you!
The managedObjects represented in the array have no sense of their position in the array. Therefore rearranging their place is changing their visible position but not their position in the database.
If you want to sort then you need to do some things:
Have your NSFetchRequest include an NSPredicate that sorts on a sort field
Have your moveRowAtIndexPath method not only reposition the data but also update the sort field to reflect their new position
Save the updated records to the database so that the next fetch will have the correct sort.
If you already have a fetchResultsController you can forgo the array and just use:
NSManagedObject *ammo = [fetchedResultsController objectAtIndexPath:fromIndexPath];
To get an reference to the current object.

Bad Access on Core Data deleteObject

I could use some assistance in debugging a EXC_BAD_ACCESS error received on the [context deleteObject:loan]; command. The error is received in the following delegate method:
- (void)didCancelNewLoan:(Loan *)loan {
// save the context
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
[context deleteObject:loan]; // *** EXC_BAD_ACCESS here ***
// This method is called from a the following method in a second class:
- (IBAction)cancel:(id)sender {
[delegate didCancelNewLoan:self.loan];
}
// The loan ivar is created by the original class
// in the below prepare for Segue method:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:#"NewLoan"]) {
UINavigationController *navController = (UINavigationController *)[segue destinationViewController];
LoanViewController *loanView = (LoanViewController *)[[navController viewControllers] lastObject];
loanView.managedObjectContext = self.managedObjectContext;
loanView.delegate = self;
loanView.loan = [self createNewLoan];
loanView.newLoan = YES;
}
// Finally, the loan is created in the above
// method's [self createNewLoan] command:
- (NSManagedObject *)createNewLoan {
//create a new instance of the entity managed by the fetched results controller
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
NSEntityDescription *entity = [[self.fetchedResultsController fetchRequest] entity];
NSManagedObject *newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];
[newManagedObject setValue:[NSDate date] forKey:#"timeStamp"];
CFUUIDRef uuid = CFUUIDCreate(NULL);
CFStringRef uuidstring = CFUUIDCreateString(NULL, uuid);
//NSString *identifierValue = (__bridge_transfer NSString *)uuidstring;
[newManagedObject setValue:(__bridge_transfer NSString *)uuidstring forKey:#"identifier"];
CFRelease(uuid);
CFRelease(uuidstring);
NSError *error;
[self.fetchedResultsController performFetch:&error];
NSLog(#"%i items in database", [[self.fetchedResultsController fetchedObjects] count]);
return newManagedObject;
}
Appreciate your looking at the above methods.
Guess #1: you are accessing a deallocated object. To debug: turn on zombies and see what happens.
Update: here's how you turn on zombies in Xcode 5:
Product > Scheme > Edit Scheme, select Diagnostics tab, check "Enable Zombie Objects"
for older Xcode
, edit your build settings, add and enable these arguments in your build scheme:
Guess #2: you have a multithreaded app and you are accessing a managed object context from different threads, which is a no no.
You can add an assert before your delete:
assert( [ NSThread isMainThread ] ) ;
From looking at your code above, there's nothing that stands out as being done incorrectly.
I am wondering whether you are dealing with two different managed object contexts without realising it? You will have to set some breakpoints where you create the Loan object and see if that might be the case.
Also why do you have to get a reference to the context via fetchedResultsController if you already have a declared property for it in self.managedObjectContext ?
The other thing is why do you need to call the fetchedResultsController to performFetch: again when you create a new Loan object? Is your data presented in a table view and have you implemented the NSFetchedResultsController delegate methods?
That call seems unnecessary and it may be causing issues with the cache created by the fetch. See section "Modifying the fetch request" under this link http://developer.apple.com/library/ios/documentation/CoreData/Reference/NSFetchedResultsController_Class/Reference/Reference.html#//apple_ref/doc/uid/TP40008227-CH1-SW24
Finally, try your delete operation directly in the view controller that received the action rather than pass it to the delegate (just to eliminate the possibility that something has been dealloc'd without you knowing).
Here's what I'd do:
- (IBAction)cancel:(id)sender
{
NSError *error;
NSManagedObjectContext *context = [self.loan managedObjectContext];
[context deleteObject:self.loan];
if (![context save:&error])
NSLog (#"Error saving context: %#", error);
}
I got a Bad Access because a deallocated UIViewController was a delegate of a NSFetchedResultsController it had.
The NSFetchedResultsController was deallocated - but when settings a delegate, it observes NSManagedObjectContext for changes, so when NSManagedObjectContext was saved - a bad access would occur when trying to notify the NSFetchedResultsController about the change.
Solution is to clear delegate of NSFetchedResultsController upon deallocation.
- (void)dealloc {
fetchedResultsController.delegate = nil;
}

CoreData DetailTableView BAD_ACCESS Error

Maybe I'm not going about showing a detail for a selected row using CoreData, but I can't figure out why I'm getting a "BAD_ACCESS" error. I've googled around and can't find what I'm looking for.
Basically I use CoreData to populate the data for a Table View. It retrieves all of the title attributes for all of the entities. When the user clicks on a row, I have a Detail View that needs to show the description for that entity. I think I need to make a new NSManagedObjectContext and a new NSEntityDescription for a new NSFetchRequest in my DetailViewController and then use a NSPredicate to say "where title = [user selected title]". I get an error when I select a row. See code:
- (void)viewDidLoad
{
// Do any additional setup after loading the view from its nib.
// Get the objects from Core Data database
Caregiver_Activity_GuideAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context = [appDelegate managedObjectContext];
NSEntityDescription *entityDescription = [NSEntityDescription
entityForName:#"Definition"
inManagedObjectContext:context];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entityDescription];
NSPredicate *pred = [NSPredicate predicateWithFormat:#"(title = %#)", self.title];
[request setPredicate:pred];
NSError *error;
NSArray *objects = [context executeFetchRequest:request error:&error];
if (objects == nil) {
NSLog(#"There was an error!");
// Do whatever error handling is appropriate
}
for (NSManagedObject *oneObject in objects) {
[definitionDescriptionTextView setText:[oneObject valueForKey:#"desc"]];
}
[objects release];
[request release];
[super viewDidLoad];
}
I comment out that code and everything works. But when I try to debug with breakpoints, nothing catches. So I'm more confused.
I know CoreData is probably overkill for what I'm doing but this is a learning app for me.
EDIT: I didn't include that I'm using a sqlite database that is pre-populated with the entities.
You can also download my project on my github page.
Normally, with a Core Data backed Master-Detail interface, you don't fetch for the Detail view.
When you select a row in the Master tableview, you are selecting a particular managed object instance. You then pass that managed object instance to the detail view. There is no need to refetch the object that you selected in the tableview.
A good example of this would be the Contacts app. The Master tableview would be a list of Contact objects (displaying the name.) When you select a row, the Master tableview controller takes the specific Contact object associated with the selected row and then passes it to the Detail view controller which then populates the Detail view using data taking from the properties of the passed Contact object.
So, that entire code block where the error occurs is unnecessary.
However, the immediate error in this code is that you are releasing an object you didn't create. In this line:
NSArray *objects = [context executeFetchRequest:request error:&error];
... you are not creating a NSArray instance with a init, new or create method. Instead, you are merely receiving an autoreleased NSArray instance created and returned by the context NSManagedObjectContext instance. When you release an object you did not create here:
[objects release];
... you cause the crash.
Conversely, you do create a NSFetchRequest here:
NSFetchRequest *request = [[NSFetchRequest alloc] init];
... because you used init so you do have to balance that with:
[request relwase];
BTW, this type of code should not be put in viewDidLoad as the method is only called when the view is read in the first time from the nib file on disk. That is only guaranteed to happen once as the view may remain in memory when the user switches to another view. Instead, put code that needs to run each time the view appears in viewWillAppear.

Adding single row one by one in tableview from core data of iPhone

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.

Handle row deletion in UITableViewController

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