How do I create an NSFetchedPropertyDescription programmatically? - objective-c

I have a pre-existing NSManagedObjectModel that I created with the Xcode GUI. I want to create a sorted fetched property, which Xcode 3.2's GUI doesn't support. I do all of this before creating my NSPersistentStoreCoordinator because I know you can't modify a NSManagedObjectModel after an object graph manager has started using it. I created the NSFetchedPropertyDescription thusly:
NSManagedObjectModel *managedObjectModel = ... // fetch from my mainBundle
NSEntityDescription *fetchedPropertyEntityDescription = [entitiesByName objectForKey:#"MyEntity"];
NSFetchRequest *fetchRequest = [[[NSFetchRequest alloc] init] autorelease];
[fetchRequest setEntity:fetchedPropertyEntityDescription];
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:#"myPredicateProperty == $FETCH_SOURCE"]];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:#"mySortProperty" ascending:YES]]];
NSFetchedPropertyDescription *fetchedPropertyDescription = [[[NSFetchedPropertyDescription alloc] init] autorelease];
[fetchedPropertyDescription setFetchRequest:fetchRequest];
[fetchedPropertyDescription setName:#"myFetchedProperty"];
NSEntityDescription *entityDescription = [entitiesByName objectForKey:#"MyFetchSourceEntity"];
[entityDescription setProperties:[[entityDescription properties] arrayByAddingObject:fetchedPropertyDescription]];
When I call
[fetchedPropertyDescription setFetchRequest:fetchRequest];
I get the following exception:
NSInvalidArgumentException: Can't use fetch request with fetched property description (entity model mismatch).

You can't alter a managed object model once it has been used to create an object graph i.e. after there is context or a store that uses it. The model defines the properties and relationships of all the objects in the graph. If you change it on the fly the graph turns into gibberish.
This applies to fetched properties as well. From the NSFetchProperyDescription docs:
Fetched Property descriptions are
editable until they are used by an
object graph manager. This allows you
to create or modify them dynamically.
However, once a description is used
(when the managed object model to
which it belongs is associated with a
persistent store coordinator), it must
not (indeed cannot) be changed. This
is enforced at runtime: any attempt to
mutate a model or any of its subjects
after the model is associated with a
persistent store coordinator causes an
exception to be thrown. If you need to
modify a model that is in use, create
a copy, modify the copy, and then
discard the objects with the old
model.

I needed to add the NSFetchedPropertyDescription to the NSEntityDescription before setting the NSFetchRequest on the NSFetchedPropertyDescription.
The proper steps are below:
NSManagedObjectModel *managedObjectModel = ... // fetch from my mainBundle
NSEntityDescription *fetchedPropertyEntityDescription = [entitiesByName objectForKey:#"MyEntity"];
NSFetchRequest *fetchRequest = [[[NSFetchRequest alloc] init] autorelease];
[fetchRequest setEntity:fetchedPropertyEntityDescription];
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:#"myPredicateProperty == $FETCH_SOURCE"]];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:#"mySortProperty" ascending:YES]]];
NSFetchedPropertyDescription *fetchedPropertyDescription = [[[NSFetchedPropertyDescription alloc] init] autorelease];
//DON'T DO THIS HERE, AN ERROR WILL OCCUR
//[fetchedPropertyDescription setFetchRequest:fetchRequest];
//
[fetchedPropertyDescription setName:#"myFetchedProperty"];
NSEntityDescription *entityDescription = [entitiesByName objectForKey:#"MyFetchSourceEntity"];
[entityDescription setProperties:[[entityDescription properties] arrayByAddingObject:fetchedPropertyDescription]];
//DO THIS HERE INSTEAD
[fetchedPropertyDescription setFetchRequest:fetchRequest];

Related

Sometimes NSArrayController is not refreshing NSTableView after core data delete/update

I am using NSArrayController Binding to populate NSTableView from core data. NSArrayController is connected to mainQueueConcurrencyType managed object context(main managed object).
parent of main managed object context is privateQueueConcurrencyType (background managed object context). Save call on main managed object context will push changes to background managed object context and save on background managed object context will save to persistent store.
Prepares contents and Editable are enabled in xib for NSArrayController
Core data table :
Path
Date
Status
I have unique constraints added to path.
Sometimes NSArrayController is not removing deleted object from arranged object after core data save.
[context performBlock:^{
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"entity" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"path ==%#", path];
[fetchRequest setPredicate:predicate];
NSError *error = nil;
NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&error];
SyncStatusEntry *syncStatus = [fetchedObjects firstObject];
NSInteger status = syncStatus.status.integerValue;
context deleteObject:syncStatus];
[context save:nil];
// Sometimes NSArrayController still have this object
}];
Can anyone please help me out?
Apparently NSArrayController is notified when something changes in the context. This notification is sent in mergeChangesFromContextDidSaveNotification: which is automatically called if automaticallyMergesChangesFromParent is YES.

Add empty section to NSFetchedResultsController

I am making a view just like the iPhone Calendar ListView. I am using core data and getting appointments and grouping them by date.
However just like in the iPhone listview I need to add a blank section for today even if there are no appointments. I cannot figure out how to do this for a section with no appointments since I am doing the sorting before I create the grouping.
How would I add an empty section to the NSFetchedResultsController and have it resorted so that today's date is in the correct spot and not at the end of the list?
- (NSFetchedResultsController *)fetchedResultsController {
/*
Set up the fetched results controller.
*/
// Create the fetch request for the entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Appointments" inManagedObjectContext:[[CoreDataHelper sharedInstance] managedObjectContext]];
[fetchRequest setEntity:entity];
//[fetchRequest setIncludesPendingChanges:YES];
// Set the batch size to a suitable number.
//[fetchRequest setFetchBatchSize:20];
// Sort using the date / then time property.
NSSortDescriptor *sortDescriptorDate = [[NSSortDescriptor alloc] initWithKey:#"date" ascending:YES];
NSSortDescriptor *sortDescriptorTime = [[NSSortDescriptor alloc] initWithKey:#"start_time" ascending:YES selector:#selector(localizedStandardCompare:)];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptorDate, sortDescriptorTime, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
// Use the sectionIdentifier property to group into sections.
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:[[CoreDataHelper sharedInstance] managedObjectContext] sectionNameKeyPath:#"date" cacheName:nil];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
return fetchedResultsController;
}
You can't have empty sections with NSFetchedResultsController - that's just the way it is designed at the moment, and I would call it a limitation :)
This problem has been encountered and addressed by a Timothy Armes who has created a class TAFetchedResultsController, which allows for empty sections. It's a replacement for NSFetchedResultsController. It also allows you to sort your sections on fields which aren't the section name (quite handy)
However, you will need to make changes to your Core Data model - it's not quite a drop in replacement.
https://github.com/timothyarmes/TAFetchedResultsController
But it does work well, and will solve your problem if you are willing to re-do your data model.
Hope this helps :)

CoreData: findAll returns objects, but simple fetchRequest does return 0 objects

I am using CoreData and executing the following command returns a number of objects:
[Contact findAll];
However when I setup a simple NSFetchrequest like so, I get 0 objects.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
fetchRequest.entity = [NSEntityDescription entityForName:#"Contact" inManagedObjectContext:self.context];
fetchRequest.sortDescriptors = [NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:#"title" ascending:YES]];
self.fetchedResultsController = [[NSFetchedResultsController alloc]
initWithFetchRequest:fetchRequest
managedObjectContext:self.context
sectionNameKeyPath:nil
cacheName:nil];
NSError *error;
NSArray *array = [self.context executeFetchRequest:fetchRequest error:&error];
Please tell me what I am doing wrong here. Since I don't have a Predicate set, I would expect to get the same number of objects, but I get an array with 0 objects and error = nil.
Please note that self.context is not nil.
If you're using RESTKit, I'm guessing that RESTKit is setting up its own Core Data context/store and when you're doing it manually, you're using the default ones provided by Apple's Xcode template. Stop using self.context and get the context from RESTKit, or use one of RESTKit's convenience methods such as objectsWithFetchRequest:.
RestKit as of 0.10 maintains a per-thread object store. This can be tricky if RestKit hasn't finished with your data (ie: your RKRequestQueue has finished but it's still processing your mappings).
You can access a foreign store with the Active Record categories, ie:
NSManagedObjectContext *restContext = [[[RKObjectManager sharedManager] objectStore] managedObjectContextForCurrentThread];
NSArray *allTexts = [TextEntity findAllInContext:restContext];
Hope this helps!

IOS Core data fetch-request - sorting

I am using a background thread to fetch limited number of records sorted with date.
Everything works well until I delete a record in the UI thread (tableview ).
//this is done in the background thread
NSFetchRequest *frequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"MyEntity"
inManagedObjectContext:self.managedObjectContext];
[frequest setEntity:entity];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc]
initWithKey:#"date"
ascending:NO];
NSArray *descriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[frequest setSortDescriptors:descriptors];
[frequest setFetchOffset:fetchOffset];
[frequest setFetchLimit:20];
[frequest setResultType:NSManagedObjectIDResultType];
NSError *fetchError;
NSMutableArray *mutableFetchResults = [[self.managedObjectContext executeFetchRequest:frequest
error:&fetchError] mutableCopy];
The background thread is registered for NSManagedObjectContextDidSaveNotification and performs the following selector
//this is done in the background thread
-(void) didSavePersistenceStore:(NSNotification *)notification
{
[self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
}
The problem: After deleting a record, subsequent fetch results are not sorted with date anymore.
What am I missing ?
First, make sure you do not use the managedObjectContext from the wrong thread. You can call performBlock to do it in the proper context.
Second, the fetch descriptor you use for the fetch does not persist. So, unless you keep fetching with the same sorting criteria, you won't get it that way.
If you want that behavior, use a fetch results controller. It will maintain your desired view into the database.
From Apple documentation:
If you set the value to NSManagedObjectIDResultType, this will demote any sort orderings to “best efforts” hints if you do not include the property values in the request.
You need to write all changes to persistent store or set includesPendingChanges to NO.

customizing localizedCaseInsensitiveCompare

I am sorting and sectioning a tableview using the first letter of an array of strings, just like the Contacts app does. I'm using Core Data and NSFetchedResultsController to drive the tableview. I use the first letter of the word, and if the letter isn't Only issue is, when I use a sort description with localizedCaseInsensitiveCompare:, it seems to generate the list of sections with: '#', 'A', 'B' ... 'Z'. I want the # to come at the end of the list, not the first (just like the Contacts app, again). Is there any creative way I could accomplish this?
Here's how I'm creating the NSFetchedResultsController:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Person" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
[fetchRequest setFetchBatchSize:100];
NSSortDescriptor *sortDescriptorLetter = [[NSSortDescriptor alloc] initWithKey:#"sectionLetter" ascending:YES selector:#selector(localizedCaseInsensitiveCompare:)];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptorLetter, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:#"sectionLetter" cacheName: nil]; // NOTE: set the cache name to nil for testing ...
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error])
...
Rather than using the ‑localizedCaseInsensitiveCompare: selector to compare the objects, you should instead initialise your NSSortDescriptor using the +sortDescriptorWithKey:ascending:comparator: class method.
This method allows you to pass in an NSComparator which is a block that contains code to compare two objects and sort them. You can compare them any way you like.
If you've never used blocks before, this will be helpful.
Apparently you can't customize sort descriptors when using the NSFetchedResultsController. You have to go with the standard sorting selectors, like "localizedCaseInsensitiveCompare".
The way I ended up doing this is to take the NSFetchedResultsController results and tweaking the boilerplate code for setting up the tableView with my own mapping of indexPaths, where I off-set the 0th section to the last section of the table. It's a bit of work, and it might just be better to forget using NSFetchedResultsController and load all the objects directly.