IOS Core data fetch-request - sorting - objective-c

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.

Related

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!

iPhone4 iOS5 NSFetchedResultsController how to pick batch size?

I got 5 fetched results controllers now, and am adding my first one to actually work with data, rather than just displaying it. I expect the controller to manage up to 150 objects. What batch size should I choose to work with up to 5 objects a time, in sequence? Is it 5?
- (NSFetchedResultsController *)estimatorEventsController
{
if (__estimatorEventsController != nil)
{
return __estimatorEventsController;
}
/*
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:#"EstimatorEvent" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:36];
// Edit the sort key as appropriate.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"epoch" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:#"EstimatorEvents"];
aFetchedResultsController.delegate = self;
self.estimatorEventsController = aFetchedResultsController;
NSError *error = nil;
if (![self.estimatorEventsController performFetch:&error])
{
NSLog(#"Unresolved error __remindersController %#, %#", error, [error userInfo]);
// abort();
}
return __estimatorEventsController;
}
I appreciate your help!
Alex,
I have 800+ items in my fetched results controllers and don't batch the fetches. Don't worry about 150 items ... you know, premature optimization and all that.
The other thing to remember is the the batch limit is on the fetch request not the fetched results controller. This distinction is important because the fetch request is a lower level item that can easily trigger a results array containing thousands of items. One must be careful with queries of such sizes. 150 rows is just not a big deal.
Andrew
I don't have a terribly informed answer, but it seems that most guides appear to be proposing that you load about two to three times as many cells as will be appearing on the screen at any one time, so if you have five visible in the table at a time, then perhaps 15? It is a balance between the benefits of not loading everything at once, versus not having too many fetch requests as they have some load and latency and making sure you already have some cells that are not yet on screen pre-fetched to be responsive during sudden rapid scrolling.

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.

How do I create an NSFetchedPropertyDescription programmatically?

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];