Im trying to retrieve an array of sorted objects from my Core Data backed NSArrayController. Once the ManagedObjectContext is ready I fire a Notification which the object owning the NSArrayController listens for and then sets up the fetch request with sortDescriptors. However the data is NEVER sorted. I've been pounding my head on this for several hours now.
I get the expected data in the array but it's not sorted at all. Anyone have any ideas where I am going wrong?
// Elsewhere in code
...
_charts = [[NSArrayController alloc] init];
_charts.entityName = #"Charts";
...
// When Cored Data and Managed Object Context is ready
-(void)mangagedObjectContextReady
{
NSManagedObjectContext *moc = ((AppDelegate *)[NSApp delegate]).managedObjectContext;
_charts.managedObjectContext = moc;
_charts.usesLazyFetching = NO;
_charts.automaticallyPreparesContent = YES;
_charts.automaticallyRearrangesObjects = YES;
// Create fetch request for data
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
fetchRequest.entity = [NSEntityDescription entityForName:#"Charts" inManagedObjectContext:moc];
// Create the sort descriptors for the Charts entity
// Name and chartType are properties on the entity "Charts"
NSSortDescriptor *nameDescriptor = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES selector:#selector(caseInsensitiveCompare:)];
NSSortDescriptor *chartTypeDescriptor = [[NSSortDescriptor alloc] initWithKey:#"chartType" ascending:YES selector:#selector(caseInsensitiveCompare:)];
fetchRequest.sortDescriptors = #[nameDescriptor, chartTypeDescriptor];
NSError *error = nil;
BOOL success = [_charts fetchWithRequest:fetchRequest merge:NO error:&error];
if(!success)
NSLog(#"Error %#:", [error localizedDescription]);
else {
[_charts rearrangeObjects];
[_tv reloadData];
}
I use this method to get an array or sorted objects.. but data is not sorted as per above sort descriptors?
[_charts.arrangedObjects objectAtIndex:row]
_charts.sortDescriptors = #[nameDescriptor, chartTypeDescriptor];
Related
I have an app that stores information for golf rounds. I have three Entities that I am persisting data to: Rounds, Courses, and Tees. I structured the schema in this way because there is a two-many relationship between Rounds and Courses, and a Course can have multiple Tees (blue, white, gold, etc.)
I have a UITableview that I would like to display the results of each round in. However, I would like to display data from the Rounds Entity as well as the Courses Entity. Ideally I would also like to display the Tee for that round as well,but it's not a priority.
My question is, how do I use a FetchResultsController to get data from the three entities and display it in a single cell of a UITableview?
Here is how I am saving the data to the Entities:
HandicapAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext* context = [appDelegate managedObjectContext];
NSEntityDescription *roundsEntity = [NSEntityDescription entityForName:#"Rounds" inManagedObjectContext:context];
NSFetchRequest *request =[[NSFetchRequest alloc]init];
[request setEntity:roundsEntity];
Rounds * rounds = [NSEntityDescription insertNewObjectForEntityForName:#"Rounds" inManagedObjectContext:context];
[rounds setValue:score forKey:#"roundScore"];
[rounds setValue:date forKey:#"roundDate"];
[rounds setValue:differential forKey:#"roundDifferential"];
Courses * courses = [NSEntityDescription insertNewObjectForEntityForName:#"Courses" inManagedObjectContext:context];
[courses setValue:rating forKey:#"courseRating"];
[courses setValue:slope forKey:#"courseSlope"];
[courses setValue:courseName forKey:#"courseName"];
rounds.courses = courses;
Tee * tee = [NSEntityDescription insertNewObjectForEntityForName:#"Tee" inManagedObjectContext:context];
[tee setValue:teeColor forKey:#"teeColor"];
courses.tees = tee;
NSError *error;
[context save:&error];
And then this is my FetchedResultsController
if (_fetchedResultsController != nil)
{
return _fetchedResultsController;
}
// Create the fetch request for the entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
fetchRequest.entity = [NSEntityDescription entityForName:#"Rounds" inManagedObjectContext:self.managedObjectContext];
// Set the batch size
[fetchRequest setFetchBatchSize:20];
// Set the sort descriptor
NSSortDescriptor * date = [[NSSortDescriptor alloc] initWithKey: #"roundDate"
ascending: NO];
NSArray * sortDescriptors = [NSArray arrayWithObjects: date, nil];
fetchRequest.sortDescriptors = sortDescriptors;
// Initialize fetched results controller - creates cache
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc]
initWithFetchRequest: fetchRequest
managedObjectContext: self.managedObjectContext
sectionNameKeyPath: nil
cacheName: #"Master"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
// handle errors
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&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();
}
return _fetchedResultsController;
How do I fetch the course name and tee (this will be the tee color) for the round in the tableview cell?
Thanks!!
Just follow the relationships. If you've got a one-to-many relationship between rounds and courses, you'd need to have a relationship on courses like rounds and the inverse relationship on rounds like course. So, if you've generated subclasses for your entities, you'd access the course name like this:
NSString *courseName = round.course.name;
I need to perform a fetch from two different entities, merge them, and then sort them based on a field that both entities have: 'lastModifiedDate'. lastModifiedDate is NSDate
NSFetchRequest *fetchRequest1 = [[NSFetchRequest alloc] init];
NSFetchRequest *fetchRequest2 = [[NSFetchRequest alloc] init];
NSEntityDescription *entity1 = [NSEntityDescription entityForName:#"Entity1" inManagedObjectContext:self.managedObjectContext];
[fetchRequest1 setEntity:entity1];
NSEntityDescription *entity2 = [NSEntityDescription entityForName:#"Entity2" inManagedObjectContext:self.managedObjectContext];
[fetchRequest2 setEntity:entity2];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"lastModeifiedDate" ascending:NO];
NSArray *sortDescriptors = [NSArray arrayWithObjects:sortDescriptor, nil];
[fetchRequest1 setSortDescriptors:sortDescriptors];
[fetchRequest2 setSortDescriptors:sortDescriptors];
[self.managedObjectContext executeFetchRequest:fetchRequest onSuccess:^(NSArray *results) {
[self.refreshControl endRefreshing];
self.objects = results; //objects is an array #property
[self.tableView reloadData];
} onFailure:^(NSError *error) {
[self.refreshControl endRefreshing];
NSLog(#"An error %#, %#", error, [error userInfo]);
}
I'm stuck here. I need to merge fetchRequest1 and fetchRequest2, and have the objects display in a tableViewController in descending order. Thanks.
You can not merge two fetch requests, but you could merge and sort the fetched arrays:
NSArray *results1 = ...; // from first fetch request;
NSArray *results2 = ...; // from second fetch request;
NSMutableArray *merged = [[NSMutableArray alloc] init];
[merged addObjectsFromArray:results1];
[merged addObjectsFromArray:results2];
[merged sortUsingDescriptors:#[sortDescriptor]];
Alternatively, you could define an entity "Entity", make that the parent entity
for "Entity1" and "Entity2", and define all common properties (such as
"lastModifiedDate") in the parent entity. Then you can fetch and sort "Entity" objects.
(A possible disadvantage of entity inheritance is that Core Data uses a single table for all
"Entity", "Entity1" and "Entity2" objects. This is not optimal if there are many
properties that are not common to both.)
this is the code in my AppDelegate.m:
-(IBAction)fetch:(id)sender{
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Foo" inManagedObjectContext:[self managedObjectContext]];
[fetchRequest setEntity:entity];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"title == 'some title'"];
[fetchRequest setPredicate:predicate];
NSError *error = nil;
NSArray *fetchedObjects = [[self managedObjectContext] executeFetchRequest:fetchRequest error:&error];
if (fetchedObjects == nil) {NSLog(#"Error: %#", error);}
NSMutableArray *fooArray = [[NSMutableArray alloc]init];
for (Foo *f in fetchedObjects) {
//here another fastenum for a to-many relationship
for(Bar *b in f.relationship){
[fooArray addObject:b.title];
}
}
Everytime I perform the fetch action, even if I've changed the app.storedata file via UI and checked the changes in finder, the result is always the same until i quit the application. After a restart, the fetch result is up to date and aligned with the app.storedata file. The fooArray count is always the same, regardless if I add some entries in the entities and coredata save everything.
I've tried with [fetchRequest setIncludesPendingChanges:YES] but it doesn't affect the behaviour.
How to update the fetch result while the app is running?
UPDATE: i've "solved" the problem with this workaround:
-(IBACTION)fetch:(id)sender{
_managedObjectContext = nil;
_persistenStoreCoordinator = nil;
//rest of the code...
Is this workaround a final solution? Is there a more "correct" way to solve this problem?
Hi I set up my own coredata app, or I tried...
First I created the xdatamodel and generated the Modelclasses, after this I implemented all the function of core-data in AppDelegate which I found in a generated project. Finally I copied the fetchedResultsController in my TableViewController.
fetchedResultsController
- (NSFetchedResultsController *)fetchedResultsController {
if (fetchedResultsController_ != nil) {
return fetchedResultsController_;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"ParameterGroup" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
[fetchRequest setFetchBatchSize:20];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:#"Root"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
[aFetchedResultsController release];
[fetchRequest release];
[sortDescriptor release];
[sortDescriptors release];
NSError *error = nil;
if (![fetchedResultsController_ performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return fetchedResultsController_;
}
First I checked if the managedObjectsController is != nil, it has a address
Then I copied the EntityName from my xdatamodel in entityForName,
but NSEntityDescricption entity is nil.
And if I just create a new object the exception says, that the entity doesn't exist
Do I have to connect the xdatamodel to my project?
Hope you can help me
Thanks a lot!!!
The most common cause of this problem is simply misspelling the entity name wrong in the code such that it doesn't match the entity name in the data model.
Copy and paste the entity name from the model to the code and see if that fixes the problem.
The simplest way to solve this, given that you haven't done a lot coding on non-core-data parts, is probably to create a new project where you check the box for "Use Core Data". If you're going to use a Navigation Bar, choose this as your template. If I recall correctly, this will generate a table view with all functions needed. You'll have to modify the datamodel (generated).
Remark that you'll have to delete the app from the Simulator if it is installed and you change the datamodel (otherwise the generated data will not be consistent with the datamodel and the app will crash)
I know I can set default values either in the datamodel, or in the -awakeFromInsert method of the entity class. For example, to make a "date" property default to the current date:
- (void) awakeFromInsert
{
NSDate *now = [NSDate date];
self.date = now;
}
How though can I make an "idNumber" property default to one greater than the previous object's idNumber?
Thanks, Oli
EDIT: Relevant code for my attempt (now corrected)
- (void) awakeFromInsert
{
self.idNumber = [NSNumber numberWithInt:[self maxIdNumber] + 1];
}
-(int)maxIdNumber{
NSManagedObjectContext *moc = [self managedObjectContext];
NSEntityDescription *entityDescription = [NSEntityDescription entityForName:#"Flight" inManagedObjectContext:moc];
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
[request setEntity:entityDescription];
// Set example predicate and sort orderings...
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"idNumber > %#", [NSNumber numberWithInt:0]];
[request setPredicate:predicate];
[request setFetchLimit:1];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"idNumber" ascending:NO];
[request setSortDescriptors:[NSArray arrayWithObject:sortDescriptor]];
[sortDescriptor release];
NSError *error;
NSArray *array = [moc executeFetchRequest:request error:&error];
if (array == nil | array.count == 0)
{
return 0;
}
return [[[array objectAtIndex:0] valueForKey:#"idNumber"] intValue];
}
If the maxIdNumber method is called, the new object is added to the table twice!? (but with the correct idNumber). The two entries in the table are linked - editing / removing one also edits / removes the other. For this reason I believe it has something to do with the managed object context. For what its worth, the outcome (two copies) is the same no matter how many times the maxIdNumber method is called in the awakFromNib; even if self.idNumber is just set to [NSNumber numberWithInt:5] and the maxIdNumber method is just called for a throwaway variable.
Any clues??
SOLVED IT!
Ok, the problem of double entry occurs when a fetch request is performed from within the awakeFromInsert method. Quoting from the docs:
You are typically discouraged from performing fetches within an implementation of awakeFromInsert. Although it is allowed, execution of the fetch request can trigger the sending of internal Core Data notifications which may have unwanted side-effects. For example, on Mac OS X, an instance of NSArrayController may end up inserting a new object into its content array twice.
A way to get around it is to use the perfromSelector:withObject:afterDelay method as outlined here (I am only allowed to post one hyperlink :( ):http://www.cocoabuilder.com/archive/cocoa/232606-auto-incrementing-integer-attribute-in-awakefrominsert.html.
My working code is now as follows: (note, I have put the bulk of the fetching code used above into a category to tidy it up a little, this allows me to use the method fetchObjectsForEntityName:withPredicate:withFetchLimit:withSortDescriptors:)
- (void) awakeFromInsert
{
[self performSelector:#selector(setIdNumber) withObject:nil afterDelay:0];
self.date = [NSDate date];
}
-(void)setIdNumber
{
int num = 0;
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"idNumber" ascending:NO];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"idNumber > %#", [NSNumber numberWithInt:0]];
NSArray *array = [[self managedObjectContext] fetchObjectsForEntityName:#"Flight"
withPredicate:predicate
withFetchLimit:0
withSortDescriptors:[NSArray arrayWithObject:sortDescriptor]];
[sortDescriptor release];
if (array != nil & array.count != 0)
{
num = [[[array objectAtIndex:0] valueForKey:#"idNumber"] intValue];
}
num ++;
[self setIdNumber:[NSNumber numberWithInt:num]];
}
Let me know what you think!
One Approach: Create a fetch request of all instances of your entity with a limit of 1, sorted by idNumber to get the highest number.
Another Approach: Keep the highest idNumber in your store's metadata and keep incrementing it.
There are plenty of arguments for and against either. Ultimately, those are the two most common and the choice is yours.
An easier way to do that is to override the newObject method of NSArrayController:
- (id) newObject
{
id result=[super newObject];
[result setValue: [NSDate date] forKey: #"date"];
return result;
}