Indexing results from an NSFetchedResultsController - iphone

I'm having some issues with a mixed, indexed, searchable results set from an NSFetchedResults controller. I have it set up to store an indexed A-Z first initial for an entity, and then want it to display numeric first initials (i.e. # as the UILocalizedIndexCollation would do).
I have already written the code that saves a "firstInitial" attribute of an Artist object as NSString #"#" if the full name started with a number, and I seem to have gotten the code half working in my UITableViewController with a customised sort descriptor. The problem is that it only works until I quit/relaunch the app.
At this point, the # section from the fetched results appears at the top. It will stay there until I force a data change (add/remove a managed object) and then search for an entry, and clear the search (using a searchDisplayController). At this point the section re-ordering will kick in and the # section will be moved to the bottom...
I'm obviously missing something/have been staring at the same code for too long. Alternatively, there's a much easier way of doing it which I'm not aware of/can't find on Google!
Any help would be appreciated!
Thanks
Sean
The relevant code from my UITableViewController is below.
- (void)viewDidLoad
{
// ----------------------------------
// Various other view set up things in here....
// ...
// ...
// ----------------------------------
NSError *error;
if (![[self artistResultsController] performFetch:&error]) {
// Update to handle the error appropriately.
NSLog(#"Failed to fetch artists: %#, %#", error, [error userInfo]);
exit(-1); // Fail
}
}
- (NSFetchedResultsController *)artistResultsController {
if (_artistResultsController != nil) {
return _artistResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"Artist" inManagedObjectContext:_context];
[fetchRequest setEntity:entity];
NSSortDescriptor *initialSort = [[NSSortDescriptor alloc]
initWithKey:#"firstInitial"
ascending:YES
comparator:^(id obj1, id obj2) {
// Various number conditions for comparison - if it's a # initial, then it's a number
if (![obj1 isEqualToString:#"#"] && [obj2 isEqualToString:#"#"]) return NSOrderedAscending;
else if ([obj1 isEqualToString:#"#"] && ![obj2 isEqualToString:#"#"]) return NSOrderedDescending;
if ([obj1 isEqualToString:#"#"] && [obj2 isEqualToString:#"#"]) return NSOrderedSame;
// Else it's a string - compare it by localized region
return [obj1 localizedCaseInsensitiveCompare:obj2];
}];
NSSortDescriptor *nameSort = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObjects:initialSort, nameSort, nil]];
[fetchRequest setFetchBatchSize:20];
NSFetchedResultsController *theFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:_context
sectionNameKeyPath:#"firstInitial"
cacheName:nil];
self.artistResultsController = theFetchedResultsController;
_artistResultsController.delegate = self;
[nameSort release];
[initialSort release];
[fetchRequest release];
[_artistResultsController release];
return _artistResultsController;}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
if (tableView == self.searchDisplayController.searchResultsTableView) {
return nil;
} else {
return [[[_artistResultsController sections] objectAtIndex:section] name];
}
}
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView
{
if (tableView == self.searchDisplayController.searchResultsTableView) {
return nil;
} else {
return [[NSArray arrayWithObject:UITableViewIndexSearch] arrayByAddingObjectsFromArray:
[[UILocalizedIndexedCollation currentCollation] sectionIndexTitles]];
}
}
- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index
{
if (tableView == self.searchDisplayController.searchResultsTableView) {
return 0;
} else {
if (title == UITableViewIndexSearch) {
[tableView scrollRectToVisible:self.searchDisplayController.searchBar.frame animated:NO];
return -1;
}
else {
for (int i = [[_artistResultsController sections] count] -1; i >=0; i--) {
NSComparisonResult cr =
[title localizedCaseInsensitiveCompare:
[[[_artistResultsController sections] objectAtIndex:i] indexTitle]];
if (cr == NSOrderedSame || cr == NSOrderedDescending) {
return i;
}
}
return 0;
}
}
}
EDIT: Forgot to mention - my search filter is using a predicate on the fetchedResults controller, so this causes a new fetch request, like so
- (void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope {
NSFetchRequest *aRequest = [_artistResultsController fetchRequest];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"name BEGINSWITH[cd] %#", searchText];
// set predicate to the request
[aRequest setPredicate:predicate];
// save changes
NSError *error = nil;
if (![_artistResultsController performFetch:&error]) {
NSLog(#"Failed to filter artists: %#, %#", error, [error userInfo]);
abort();
}
}

I ended up going about fixing this a different way.
SortDescriptors seem to have issues with running a custom sort when you are also using CoreData with SQLite for your backend storage. I tried a few things; NSString categories with a new comparison method, the compare block as listed above, and refreshing the table multiple times to try and force an update with the sort criterion.
In the end, I couldn't force the sort descriptor to do an initial sort, so I changed the implementation. I set the firstInitial attribute for artists whose names began with numerics to 'zzzz'. This means that CoreData will sort this correctly (numerics last) off the bat.
After doing this, I then hardcoded my titleForHeaderInSection method to return # for the title if appropriate, as below:
if ([[[[_artistResultsController sections] objectAtIndex:section] indexTitle] isEqualToString:#"zzzz"]) return [NSString stringWithString:#"#"];
return [[[_artistResultsController sections] objectAtIndex:section] indexTitle];
Essentially this means it's sorting numbers into a 'zzzz' grouping, which should be last, and I'm just ignoring that title and saying the title is # instead.
Not sure if there's a better way to do this, but it keeps all of the sorting inside CoreData, which is probably more efficient/scalable in the long run.

Related

Optimizing this Core Data request

I have an entity in Core Data named MusicInterest. I have to add 5000 or so of these at a time and my current process is to query to see if the MusicInterest exists already, if not create a new one.
It seems this requires 5000 trips to the store to see if each title exists. There are also, of course, insert trips, but the 5000 queries is what's slowing me down.
Each FacebookFriend will have multiple music interests, and I enumerate through each one using an array of string titles, calling the following code.
Any ideas how to optimize this?
+ (MusicInterest*) musicInterestForFacebookFriend:(FacebookFriend*)facebookFriend WithTitle:(NSString*)musicTitle UsingManagedObjectContext:(NSManagedObjectContext*)moc
{
// query to see if there
NSArray *matches = [self queryForMusicTitle:musicTitle moc:moc];
if (([matches count] >= 1)) {
// NSLog(#"Music already in database");
MusicInterest *existingMusic = [matches lastObject];
[existingMusic addLikedByObject:facebookFriend];
return [matches lastObject];
} else {
// create new Music Interest
MusicInterest *newMusic = [NSEntityDescription insertNewObjectForEntityForName:#"MusicInterest" inManagedObjectContext:moc];
newMusic.title = musicTitle;
[newMusic addLikedByObject:facebookFriend];
return newMusic;
}
}
+ (NSArray *)queryForMusicTitle:(NSString *)MusicTitle moc:(NSManagedObjectContext *)moc
{
// query to see if there
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"MusicInterest"];
request.predicate = [NSPredicate predicateWithFormat:#"title == %#", [NSString stringWithFormat:#"%#", MusicTitle]];
NSError *error = nil;
NSArray *matches = [moc executeFetchRequest:request error:&error];
if (error) {
NSLog(#"Error querying title in Music interest. Error = %#", error);
}
return matches;
}
UPDATE:
I employed the design suggested in the Core Data programming guide and it reduced my time from 12 seconds to 4 seconds (still needs some optimization in other areas :)
The guide only includes half the sample code - I thought I would share my complete implementation:
musicArray = [[music componentsSeparatedByString:#", "] sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
if (obj1 > obj2)
return NSOrderedDescending;
else if (obj1 < obj2)
return NSOrderedAscending;
return NSOrderedSame;
}];
if (musicArray) {
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"MusicInterest"];
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:#"title IN %#", musicArray]];
[fetchRequest setSortDescriptors:
#[[[NSSortDescriptor alloc] initWithKey: #"title" ascending:YES]]];
NSError *fetchError = nil;
NSArray *musicInterestMatchingTitles = [backgroundContext executeFetchRequest:fetchRequest error:&fetchError];
if ([musicArray count] > 0) {
// walk musicArray and musicInterestsMatchingTitles in parallel
for (int i = 0; i < [musicArray count]; i++) {
NSString *title = musicArray[i];
if (i < [musicInterestMatchingTitles count]) {
MusicInterest *comparingMusicInterest = musicInterestMatchingTitles[i];
// compare each title
if (![title isEqualToString:comparingMusicInterest.title]) {
// if it doesn't exist as a ManagedObject (a MusicInterest), create one
MusicInterest *musicInterest = [MusicInterest createNewMusicInterestUsingManagedObjectContext:backgroundContext];
musicInterest.title = title;
[musicInterest addLikedByObject:friend];
} else {
// otherwise, just establish the relationship
[comparingMusicInterest addLikedByObject:friend];
}
} else {
// if there are no existing matching managedObjects, create one
MusicInterest *musicInterest = [MusicInterest createNewMusicInterestUsingManagedObjectContext:backgroundContext];
musicInterest.title = title;
[musicInterest addLikedByObject:friend];
}
}
}
}
}];
[self saveBackgroundContext:backgroundContext];
Implementing Find-or-Create Efficiently in the "Core Data Programming Guide" describes a pattern that might be useful here. The basic idea is:
Sort your list of items that you want to insert/update by some unique id that is also stored in
the database.
Perform a single fetch request that fetches all objects from the database that have an id from your list, sorted by the same id.
Now traverse your list and the array of fetched items in parallel, to find which items have to be inserted and which items already exist and can be updated.

UITableView / UISearchBar Returns Incorrect Results

I am attempting to implement searching in a UITableView. When searching, it appears that the correct number of results are returned, but I am receiving entries from the original stories array in the results, rather than searchResults. I can see that the searchResults array should be the data source, but haven't been able to figure out after tons of searching quite how to pull it off with an array of NSDictionaries. Any help is appreciated.
- (void)handleSearchForTerm:(NSString *)searchTerm {
[self setSavedSearchTerm:searchTerm];
if ([self searchResults] == nil)
{
NSMutableArray *array = [[NSMutableArray alloc] init];
[self setSearchResults:array];
[array release], array = nil;
}
[[self searchResults] removeAllObjects];
if ([[self savedSearchTerm] length] != 0)
{
for (NSDictionary *currentItem in [self stories])
{
if ([[currentItem objectForKey:#"title"] rangeOfString:searchTerm options:NSCaseInsensitiveSearch].location != NSNotFound)
{
[[self searchResults] addObject:currentItem];
}
}
}
}
[tableView isEqual:self.searchDisplayController.searchResultsTableView] is also another alternative to making and managing your own BOOL isFiltering; variable
use NSPredicate for filtering
NSPredicate* predicate = [NSPredicate predicateWithFormat:#"self.title MATCHES %#",searchTerm];
Suppose that your original array is "originalArray" so to get the filtered array use this make two more global variables
NSArray* filteredArray;
BOOL isFiltering;
Now in search bar delegate method do following
-(void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
NSPredicate* predicate = [NSPredicate predicateWithFormat:#"self.title MATCHES %#",searchTerm];
filteredArray = [[originalArray filteredArrayUsingPredicate:predicate] retain];
}
Now you need to change l'll bit your table view delegate and data source, .... for all the places where you are using
NSDictionary *currentString = [originalArray objectAtIndex:indexPath.row];
use following
NSDictionary *currentString;
if(isFiltering)
currentString = [originalArray objectAtIndex:indexPath.row];
else
currentString = [filteredArray objectAtIndex:indexPath.row];

How to filter NSFetchedResultsController (CoreData) with UISearchDisplayController/UISearchBar

I'm trying to implement search code in my CoreData-based iPhone app. I'm not sure how to proceed. The app already has an NSFetchedResultsController with a predicate to retrieve the data for the primary TableView. I want to make sure I'm on the right path before I change too much code. I'm confused because so many of the examples are array-based instead of CoreData.
Here are some questions:
Do I need to have a second NSFetchedResultsController that retrieves only the matching items or can I use the same one as the primary TableView?
If I use the same one, is it as simple as clearing the FRC cache and then changing the predicate in the handleSearchForTerm:searchString method? Does the predicate have to contain the initial predicate as well as the search terms or does it remember that it used a predicate to retrieve data in the first place?
How do I get back to the original results? Do I just set the search predicate to nil? Won't that kill the original predicate that was used to retrieve the FRC results in the first place?
If anyone has any examples of code using search with the FRC, I would greatly appreciate it!
I actually just implemented this on one of my projects (your question and the other wrong answer hinted at what to do). I tried Sergio's answer but had exception issues when actually running on a device.
Yes you create two fetch results controllers: one for the normal display and another one for the UISearchBar's table view.
If you only use one FRC (NSFetchedResultsController) then the original UITableView (not the search table view that is active while searching) will possibly have callbacks called while you are searching and try to incorrectly use the filtered version of your FRC and you will see exceptions thrown about incorrect number of sections or rows in sections.
Here is what I did: I have two FRCs available as properties fetchedResultsController and searchFetchedResultsController. The searchFetchedResultsController should not be used unless there is a search (when the search is canceled you can see below that this object is released). All UITableView methods must figure out what table view it will query and which applicable FRC to pull the information from. The FRC delegate methods must also figure out which tableView to update.
It is surprising how much of this is boilerplate code.
Relevant bits of the header file:
#interface BlahViewController : UITableViewController <UISearchBarDelegate, NSFetchedResultsControllerDelegate, UISearchDisplayDelegate>
{
// other class ivars
// required ivars for this example
NSFetchedResultsController *fetchedResultsController_;
NSFetchedResultsController *searchFetchedResultsController_;
NSManagedObjectContext *managedObjectContext_;
// The saved state of the search UI if a memory warning removed the view.
NSString *savedSearchTerm_;
NSInteger savedScopeButtonIndex_;
BOOL searchWasActive_;
}
#property (nonatomic, retain) NSManagedObjectContext *managedObjectContext;
#property (nonatomic, retain, readonly) NSFetchedResultsController *fetchedResultsController;
#property (nonatomic, copy) NSString *savedSearchTerm;
#property (nonatomic) NSInteger savedScopeButtonIndex;
#property (nonatomic) BOOL searchWasActive;
relevent bits of the implementation file:
#interface BlahViewController ()
#property (nonatomic, retain) NSFetchedResultsController *fetchedResultsController;
#property (nonatomic, retain) NSFetchedResultsController *searchFetchedResultsController;
#property (nonatomic, retain) UISearchDisplayController *mySearchDisplayController;
#end
I created a helpful method to retrieve the correct FRC when working with all of the UITableViewDelegate/DataSource methods:
- (NSFetchedResultsController *)fetchedResultsControllerForTableView:(UITableView *)tableView
{
return tableView == self.tableView ? self.fetchedResultsController : self.searchFetchedResultsController;
}
- (void)fetchedResultsController:(NSFetchedResultsController *)fetchedResultsController configureCell:(UITableViewCell *)theCell atIndexPath:(NSIndexPath *)theIndexPath
{
// your cell guts here
}
- (UITableViewCell *)tableView:(UITableView *)theTableView cellForRowAtIndexPath:(NSIndexPath *)theIndexPath
{
CallTableCell *cell = (CallTableCell *)[theTableView dequeueReusableCellWithIdentifier:#"CallTableCell"];
if (cell == nil)
{
cell = [[[CallTableCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"CallTableCell"] autorelease];
}
[self fetchedResultsController:[self fetchedResultsControllerForTableView:theTableView] configureCell:cell atIndexPath:theIndexPath];
return cell;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
NSInteger count = [[[self fetchedResultsControllerForTableView:tableView] sections] count];
return count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSInteger numberOfRows = 0;
NSFetchedResultsController *fetchController = [self fetchedResultsControllerForTableView:tableView];
NSArray *sections = fetchController.sections;
if(sections.count > 0)
{
id <NSFetchedResultsSectionInfo> sectionInfo = [sections objectAtIndex:section];
numberOfRows = [sectionInfo numberOfObjects];
}
return numberOfRows;
}
Delegate methods for the search bar:
#pragma mark -
#pragma mark Content Filtering
- (void)filterContentForSearchText:(NSString*)searchText scope:(NSInteger)scope
{
// update the filter, in this case just blow away the FRC and let lazy evaluation create another with the relevant search info
self.searchFetchedResultsController.delegate = nil;
self.searchFetchedResultsController = nil;
// if you care about the scope save off the index to be used by the serchFetchedResultsController
//self.savedScopeButtonIndex = scope;
}
#pragma mark -
#pragma mark Search Bar
- (void)searchDisplayController:(UISearchDisplayController *)controller willUnloadSearchResultsTableView:(UITableView *)tableView;
{
// search is done so get rid of the search FRC and reclaim memory
self.searchFetchedResultsController.delegate = nil;
self.searchFetchedResultsController = nil;
}
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString
{
[self filterContentForSearchText:searchString
scope:[self.searchDisplayController.searchBar selectedScopeButtonIndex]];
// Return YES to cause the search result table view to be reloaded.
return YES;
}
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchScope:(NSInteger)searchOption
{
[self filterContentForSearchText:[self.searchDisplayController.searchBar text]
scope:[self.searchDisplayController.searchBar selectedScopeButtonIndex]];
// Return YES to cause the search result table view to be reloaded.
return YES;
}
make sure that you use the correct table view when getting updates from the FRC delegate methods:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
UITableView *tableView = controller == self.fetchedResultsController ? self.tableView : self.searchDisplayController.searchResultsTableView;
[tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller
didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex
forChangeType:(NSFetchedResultsChangeType)type
{
UITableView *tableView = controller == self.fetchedResultsController ? self.tableView : self.searchDisplayController.searchResultsTableView;
switch(type)
{
case NSFetchedResultsChangeInsert:
[tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller
didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)theIndexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = controller == self.fetchedResultsController ? self.tableView : self.searchDisplayController.searchResultsTableView;
switch(type)
{
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:theIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self fetchedResultsController:controller configureCell:[tableView cellForRowAtIndexPath:theIndexPath] atIndexPath:theIndexPath];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:theIndexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
UITableView *tableView = controller == self.fetchedResultsController ? self.tableView : self.searchDisplayController.searchResultsTableView;
[tableView endUpdates];
}
Other view information:
- (void)loadView
{
[super loadView];
UISearchBar *searchBar = [[[UISearchBar alloc] initWithFrame:CGRectMake(0, 0, self.tableView.frame.size.width, 44.0)] autorelease];
searchBar.autoresizingMask = (UIViewAutoresizingFlexibleWidth);
searchBar.autocorrectionType = UITextAutocorrectionTypeNo;
self.tableView.tableHeaderView = searchBar;
self.mySearchDisplayController = [[[UISearchDisplayController alloc] initWithSearchBar:searchBar contentsController:self] autorelease];
self.mySearchDisplayController.delegate = self;
self.mySearchDisplayController.searchResultsDataSource = self;
self.mySearchDisplayController.searchResultsDelegate = self;
}
- (void)didReceiveMemoryWarning
{
self.searchWasActive = [self.searchDisplayController isActive];
self.savedSearchTerm = [self.searchDisplayController.searchBar text];
self.savedScopeButtonIndex = [self.searchDisplayController.searchBar selectedScopeButtonIndex];
fetchedResultsController_.delegate = nil;
[fetchedResultsController_ release];
fetchedResultsController_ = nil;
searchFetchedResultsController_.delegate = nil;
[searchFetchedResultsController_ release];
searchFetchedResultsController_ = nil;
[super didReceiveMemoryWarning];
}
- (void)viewDidDisappear:(BOOL)animated
{
// save the state of the search UI so that it can be restored if the view is re-created
self.searchWasActive = [self.searchDisplayController isActive];
self.savedSearchTerm = [self.searchDisplayController.searchBar text];
self.savedScopeButtonIndex = [self.searchDisplayController.searchBar selectedScopeButtonIndex];
}
- (void)viewDidLoad
{
// restore search settings if they were saved in didReceiveMemoryWarning.
if (self.savedSearchTerm)
{
[self.searchDisplayController setActive:self.searchWasActive];
[self.searchDisplayController.searchBar setSelectedScopeButtonIndex:self.savedScopeButtonIndex];
[self.searchDisplayController.searchBar setText:savedSearchTerm];
self.savedSearchTerm = nil;
}
}
FRC creation code:
- (NSFetchedResultsController *)newFetchedResultsControllerWithSearch:(NSString *)searchString
{
NSArray *sortDescriptors = // your sort descriptors here
NSPredicate *filterPredicate = // your predicate here
/*
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 *callEntity = [MTCall entityInManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:callEntity];
NSMutableArray *predicateArray = [NSMutableArray array];
if(searchString.length)
{
// your search predicate(s) are added to this array
[predicateArray addObject:[NSPredicate predicateWithFormat:#"name CONTAINS[cd] %#", searchString]];
// finally add the filter predicate for this view
if(filterPredicate)
{
filterPredicate = [NSCompoundPredicate andPredicateWithSubpredicates:[NSArray arrayWithObjects:filterPredicate, [NSCompoundPredicate orPredicateWithSubpredicates:predicateArray], nil]];
}
else
{
filterPredicate = [NSCompoundPredicate orPredicateWithSubpredicates:predicateArray];
}
}
[fetchRequest setPredicate:filterPredicate];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
[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:nil];
aFetchedResultsController.delegate = self;
[fetchRequest release];
NSError *error = nil;
if (![aFetchedResultsController 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. 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();
}
return aFetchedResultsController;
}
- (NSFetchedResultsController *)fetchedResultsController
{
if (fetchedResultsController_ != nil)
{
return fetchedResultsController_;
}
fetchedResultsController_ = [self newFetchedResultsControllerWithSearch:nil];
return [[fetchedResultsController_ retain] autorelease];
}
- (NSFetchedResultsController *)searchFetchedResultsController
{
if (searchFetchedResultsController_ != nil)
{
return searchFetchedResultsController_;
}
searchFetchedResultsController_ = [self newFetchedResultsControllerWithSearch:self.searchDisplayController.searchBar.text];
return [[searchFetchedResultsController_ retain] autorelease];
}
Some have commented that this can be done with a single NSFetchedResultsController. That's what I did, and here are the details. This solution assumes you just want to filter down the table and maintain all other aspects (sort order, cell layout, etc.) of the search results.
First, define two properties in your UITableViewController subclass (with the appropriate #synthesize and dealloc, if applicable):
#property (nonatomic, retain) UISearchDisplayController *searchController;
#property (nonatomic, retain) NSString *searchString;
Second, initialize the search bar in the viewDidLoad: method of your UITableViewController subclass:
UISearchBar *searchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0,0,self.tableView.frame.size.width,44)];
searchBar.placeholder = #"Search";
searchBar.delegate = self;
self.searchController = [[[UISearchDisplayController alloc] initWithSearchBar:searchBar contentsController:self] autorelease];
self.searchController.delegate = self;
self.searchController.searchResultsDataSource = self;
self.searchController.searchResultsDelegate = self;
self.tableView.tableHeaderView = self.searchController.searchBar;
[searchBar release];
Third, implement the UISearchDisplayController delegate methods like this:
// This gets called when you start typing text into the search bar
-(BOOL)searchDisplayController:(UISearchDisplayController *)_controller shouldReloadTableForSearchString:(NSString *)_searchString {
self.searchString = _searchString;
self.fetchedResultsController = nil;
return YES;
}
// This gets called when you cancel or close the search bar
-(void)searchDisplayController:(UISearchDisplayController *)controller willUnloadSearchResultsTableView:(UITableView *)tableView {
self.searchString = nil;
self.fetchedResultsController = nil;
[self.tableView reloadData];
}
Finally, in the fetchedResultsController method change the NSPredicate depending if
self.searchString is defined:
-(NSFetchedResultsController *)fetchedResultsController {
if (fetchedResultsController == nil) {
// removed for brevity
NSPredicate *predicate;
if (self.searchString) {
// predicate that uses searchString (used by UISearchDisplayController)
// e.g., [NSPredicate predicateWithFormat:#"name CONTAINS[cd] %#", self.searchString];
predicate = ...
} else {
predicate = ... // predicate without searchString (used by UITableViewController)
}
// removed for brevity
}
return fetchedResultsController;
}
It took me a few tries to get this working...
My key to understanding was realizing that there are two tableViews at work here. One managed by my viewcontroller and one managed by the searchviewcontroller and then I could test to see which is active and do the right thing. The documentation was helpful too:
http://developer.apple.com/library/ios/#documentation/uikit/reference/UISearchDisplayController_Class/Reference/Reference.html
Here's what I did -
Added the searchIsActive flag:
#interface ItemTableViewController : UITableViewController <NSFetchedResultsControllerDelegate, UISearchDisplayDelegate, UISearchBarDelegate> {
NSString *sectionNameKeyPath;
NSArray *sortDescriptors;
#private
NSFetchedResultsController *fetchedResultsController_;
NSManagedObjectContext *managedObjectContext_;
BOOL searchIsActive;
}
#property (nonatomic, retain) NSManagedObjectContext *managedObjectContext;
#property (nonatomic, retain) NSFetchedResultsController *fetchedResultsController;
#property (nonatomic, retain) NSString *sectionNameKeyPath;
#property (nonatomic, retain) NSArray *sortDescriptors;
#property (nonatomic) BOOL searchIsActive;
Added the synthesize in the implementation file.
Then I added these methods to for searching:
#pragma mark -
#pragma mark Content Filtering
- (void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope
{
NSFetchRequest *aRequest = [[self fetchedResultsController] fetchRequest];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"name BEGINSWITH[cd] %#", searchText];
[aRequest setPredicate:predicate];
NSError *error = nil;
if (![[self fetchedResultsController] performFetch:&error]) {
// Handle error
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}
#pragma mark -
#pragma mark UISearchDisplayController Delegate Methods
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString
{
[self filterContentForSearchText:[self.searchDisplayController.searchBar text] scope:nil];
return YES;
}
/*
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchScope:(NSInteger)searchOption
{
return YES;
}
*/
- (void)searchDisplayControllerWillBeginSearch:(UISearchDisplayController *)controller {
[self setSearchIsActive:YES];
return;
}
- (void)searchDisplayControllerDidEndSearch:(UISearchDisplayController *)controller
{
NSFetchRequest *aRequest = [[self fetchedResultsController] fetchRequest];
[aRequest setPredicate:nil];
NSError *error = nil;
if (![[self fetchedResultsController] performFetch:&error]) {
// Handle error
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
[self setSearchIsActive:NO];
return;
}
Then in controllerWillChangeContent:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
if ([self searchIsActive]) {
[[[self searchDisplayController] searchResultsTableView] beginUpdates];
}
else {
[self.tableView beginUpdates];
}
}
And controllerDidChangeContent:
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
if ([self searchIsActive]) {
[[[self searchDisplayController] searchResultsTableView] endUpdates];
}
else {
[self.tableView endUpdates];
}
}
And delete the cache when resetting the predicate.
Hope this helps.
Swift 3.0, UISearchController, NSFetchedResultsController and Core Data
This code will work on Swift 3.0 with Core Data! You'll need a single delegate method and a few lines of code for filtering and searching objects from the model. Nothing will be needed if you have implemented all of the FRC and their delegate methods as well as searchController.
The UISearchResultsUpdating protocol method
func updateSearchResults(for searchController: UISearchController) {
let text = searchController.searchBar.text
if (text?.isEmpty)! {
// Do something
} else {
self.fetchedResultsController.fetchRequest.predicate = NSPredicate(format: "( someString contains[cd] %# )", text!)
}
do {
try self.fetchedResultsController.performFetch()
self.tableView.reloadData()
} catch {}
}
That's it! Hope it helps you! Thanks
I faced with the same task and found THE SIMPLEST WAY POSSIBLE to solve it.
Shortly: you need to define one more method, very similar to -fetchedResultsController with a custom compound predicate.
In my personal case my -fetchedResultsController looks like this:
- (NSFetchedResultsController *) fetchedResultsController
{
    if (fetchedResultsController != nil)
    {
        return fetchedResultsController;
    }
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:#"Client"
                                              inManagedObjectContext:[[PTDataManager sharedManager] managedObjectContext]];
    [fetchRequest setEntity:entity];
    NSPredicate *predicate = [NSPredicate predicateWithFormat:#"agency_server_id == %#", agency.server_id];
    fetchRequest.predicate = predicate;
    NSSortDescriptor *sortByName1Descriptor = [[NSSortDescriptor alloc] initWithKey:#"lastname" ascending:YES];
    NSSortDescriptor *sortByName2Descriptor = [[NSSortDescriptor alloc] initWithKey:#"firstname" ascending:YES];
    NSSortDescriptor *sortByName3Descriptor = [[NSSortDescriptor alloc] initWithKey:#"middlename" ascending:YES];
    NSArray *sortDescriptors = [[NSArray alloc] initWithObjects: sortByName1Descriptor, sortByName2Descriptor, sortByName3Descriptor, nil];
    fetchRequest.sortDescriptors = sortDescriptors;
    fetchedResultsController = [[NSFetchedResultsController alloc]initWithFetchRequest:fetchRequest managedObjectContext:[[PTDataManager sharedManager] managedObjectContext] sectionNameKeyPath:nil cacheName:nil];
    fetchedResultsController.delegate = self;
    return fetchedResultsController;
}
As you can see I am fetching clients of one agency filtered by agency.server_id predicate. As a result I am retrieving my content in a tableView (all related to implementation of tableView and fetchedResultsController code is pretty standard) as well.
To implement searchField I am defining a UISearchBarDelegate delegate method. I am triggering it with search method, say -reloadTableView:
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
[self reloadTableView];
}
and of course definition of -reloadTableView:
- (void)reloadTableView
{
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Client"
inManagedObjectContext:[[PTDataManager sharedManager] managedObjectContext]];
[fetchRequest setEntity:entity];
NSSortDescriptor *sortByName1Descriptor = [[NSSortDescriptor alloc] initWithKey:#"lastname" ascending:YES];
NSSortDescriptor *sortByName2Descriptor = [[NSSortDescriptor alloc] initWithKey:#"firstname" ascending:YES];
NSSortDescriptor *sortByName3Descriptor = [[NSSortDescriptor alloc] initWithKey:#"middlename" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects: sortByName1Descriptor, sortByName2Descriptor, sortByName3Descriptor, nil];
fetchRequest.sortDescriptors = sortDescriptors;
NSPredicate *idPredicate = [NSPredicate predicateWithFormat:#"agency_server_id CONTAINS[cd] %#", agency.server_id];
NSString *searchString = self.searchBar.text;
if (searchString.length > 0)
{
NSPredicate *firstNamePredicate = [NSPredicate predicateWithFormat:#"firstname CONTAINS[cd] %#", searchString];
NSPredicate *lastNamePredicate = [NSPredicate predicateWithFormat:#"lastname CONTAINS[cd] %#", searchString];
NSPredicate *middleNamePredicate = [NSPredicate predicateWithFormat:#"middlename CONTAINS[cd] %#", searchString];
NSPredicate *orPredicate = [NSCompoundPredicate orPredicateWithSubpredicates:[NSArray arrayWithObjects:firstNamePredicate, lastNamePredicate, middleNamePredicate, nil]];
NSPredicate *andPredicate = [NSCompoundPredicate andPredicateWithSubpredicates:[NSArray arrayWithObjects:idPredicate, nil]];
NSPredicate *finalPred = [NSCompoundPredicate andPredicateWithSubpredicates:[NSArray arrayWithObjects:orPredicate, andPredicate, nil]];
[fetchRequest setPredicate:finalPred];
}
else
{
[fetchRequest setPredicate:idPredicate];
}
self.fetchedResultsController = [[NSFetchedResultsController alloc]initWithFetchRequest:fetchRequest managedObjectContext:[[PTDataManager sharedManager] managedObjectContext] sectionNameKeyPath:nil cacheName:nil];
self.fetchedResultsController.delegate = self;
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error])
{
NSLog(#"Unresolved error %#, %#", [error localizedDescription], [error localizedFailureReason]);
};
[self.clientsTableView reloadData];
}
This bunch of code is very similar to first, "standard" -fetchedResultsController BUT inside if-else statement here is:
+andPredicateWithSubpredicates: - using this method we can set a predicate to save results of our main first fetch in the tableView
+orPredicateWithSubpredicates - using this method we are filtering existing fetch by search query from searchBar
At the end I am setting array of predicates as a compound predicate for this particular fetch. AND for required predicates, OR for optional.
And that's all! You don't need to implement anything more.
Happy coding!
Are you using a live search?
If you are NOT, you probably want an array (or an NSFetchedResultsController) with the previous searches you used, when the user presses "search", you tell your FetchedResults to change its predicate.
Either way, you will need to rebuild your FetchedResults every time. I recommend using only one NSFetchedResultsController, since you'll have to duplicate your code a lot and you don't need to waste memory in something you're not showing.
Just make sure you have a NSString "searchParameters" variable and your FetchedResults method rebuilds it for you as needed, using the search parameters if available, you should just do:
a) set the "searchParameters" to something (or nil, if you want all the results).
b) release and set to nil the current NSFetchedResultsController object.
c) reload table data.
Here's a simple code:
- (void)searchString:(NSString*)s {
self.searchResults = s;
[fetchedResultsController release];
fetchedResultsController = nil;
[self.tableView reloadData];
}
-(NSFetchedResultsController *)fetchedResultsController {
if (fetchedResultsController != nil) {
return fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"EntityName" inManagedObjectContext:self.context];
[fetchRequest setEntity:entity];
[fetchRequest setFetchBatchSize:20];
// searchResults is a NSString*
if (searchResults != nil) {
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"name LIKE %#",searchResults];
[fetchRequest setPredicate:predicate];
}
fetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:self.context sectionNameKeyPath:nil
cacheName:nil];
fetchedResultsController.delegate = self;
[fetchRequest release];
return fetchedResultsController;
}
SWIFT 3.0
Use a textField, UISearchDisplayController is deprecated as of iOS 8, you would have to use a UISearchController. Instead of dealing with the Search Controller, why dont you create your own search mechanism? You can customize it more and have more control over it, and not have to worry about SearchController changing and/or being deprecated.
This method I use works very well, and doesnt require much code. It does require you use Core Data and implement NSFetchedResultsController, however.
First, create a TextField and register it with a method:
searchTextField?.addTarget(self, action: #selector(textFieldDidChange), for: UIControlEvents.editingChanged)
Then create your textFieldDidChange method, described in the selector when the target was added:
func textFieldDidChange() {
if let queryString = searchTextField.text {
filterList(queryString)
self.tableView.reloadData()
}
}
Then you want to filter the list in filterList() method using NSPredicate or NSCompound predicate if it is more complex. In my filterList method, I am filtering based on the name of the entity, and the name of the entities "subCategories" object (a one to many relationship).
func filterList(_ queryString: String) {
if let currentProjectID = Constants.userDefaults.string(forKey: Constants.CurrentSelectedProjectID) {
if let currentProject = ProjectDBFacade.getProjectWithID(currentProjectID) {
if (queryString != ""){
let categoryPredicate = NSPredicate(format: "name CONTAINS[c] %# && project == %#", queryString, currentProject)
let subCategoryPredicate = NSPredicate(format: "subCategories.name CONTAINS[c] %# && project == %#", queryString, currentProject)
let orPredicate = NSCompoundPredicate(type: .or, subpredicates: [categoryPredicate, subCategoryPredicate])
fetchedResultsController.fetchRequest.predicate = orPredicate
}else{
fetchedResultsController.fetchRequest.predicate = NSPredicate(format: "project == %#", currentProject)
}
do {
try fetchedResultsController.performFetch()
} catch {
print("Error: Could not fetch fetchedResultsController")
}
}
}
}
Simple Approach to Filter existing UITableView using CoreData and which is already sorted how you want.
This literally took me 5 minutes to setup and get working.
I had an existing UITableView using CoreData populated with Data from iCloud and which has pretty complicated user interactions and I didn't want to have to replicate all that for a UISearchViewController. I was able to simply add a predicate to the existing FetchRequest already used by the FetchResultsController and which filters the already sorted data.
-(void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
NSPredicate *filterPredicate;
if(searchText != nil && searchText.length > 0)
filterPredicate = [NSPredicate predicateWithFormat:#"(someField CONTAINS[cd] %#) OR (someOtherField CONTAINS[cd] %#)", searchText, searchText];
else
filterPredicate = nil;
_fetchedResultsController.fetchRequest.predicate = filterPredicate;
NSError *error = nil;
[_fetchedResultsController performFetch:&error];
[self.tableView reloadData];
}
I think Luka has a better approach for this. See LargeDataSetSample and his reason
He does not use FetchedResultsController, but uses cache when searching, hence the search results appears much faster when user types more in SearchBar
I've used his approach in my app and it works OK. Also remember if you want to work with Model object, make it as simple as possible, see my answer about setPropertiesToFetch
Here's a way of handling fetchedResults with multiple data sets that is both simple and general enough to apply almost anywhere. Simply grab your main results to an array when some condition is present.
NSArray *results = [self.fetchedResultsController fetchedObjects];
Query the array by looping through it or whatever you desire in order to create a subset of your main fetchedResults. And now you can either use the full set or subset when some condition is present.
I really liked #Josh O'Connor's approach where he does not use a UISearchController. This controller still (Xcode 9) has a layout bug that many are trying to workaround.
I did revert to using a UISearchBar instead of a UITextField, and it works quite nicely. My requirement for the search/filter is to produce an NSPredicate. This is passed to the FRC:
class ViewController: UIViewController, UITableViewDelegate,
UITableViewDataSource, UISearchBarDelegate {
#IBOutlet var searchBar: UISearchBar!
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
shouldShowSearchResults = true
if let queryString = searchBar.text {
filterList(queryString)
fetchData()
}
}
func filterList(_ queryString: String) {
if (queryString == "") {
searchPredicate = nil
}
else {
let brandPredicate = NSPredicate(format: "brand CONTAINS[c] %#", queryString)
let modelPredicate = NSPredicate(format: "model CONTAINS[c] %#", queryString)
let orPredicate = NSCompoundPredicate(type: .or, subpredicates: [brandPredicate, modelPredicate])
searchPredicate = orPredicate
}
}
...
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let request = NSFetchRequest<NSFetchRequestResult>(entityName: filmEntity)
request.returnsDistinctResults = true
request.propertiesToFetch = ["brand", "model"]
request.sortDescriptors = [sortDescriptor]
request.predicate = searchPredicate
Finally, wire up the SearchBar to its delegate.
I hope this helps others

Core Data Error "Fetch Request must have an entity"

I've attempted to add the TopSongs parser and Core Data files into my application, and it now builds succesfully, with no errors or warning messages. However, as soon as the app loads, it crashes, giving the following reason:
UPDATE: I've got it all working, but my TableView doesn't show any data, and the app doesn't respond to the following breakpoints.
Thanks.
UPDATE: Here's the new code that doesn't respond to the breakpoints.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)table {
return [[fetchedResultsController sections] count];
}
- (NSInteger)tableView:(UITableView *)table numberOfRowsInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> sectionInfo = [[fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
}
- (void)viewDidUnload {
[super viewDidUnload];
self.tableView = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextDidSaveNotification object:self.managedObjectContext];
[self.tableView reloadData];
}
- (UITableViewCell *)tableView:(UITableView *)table cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *kCellIdentifier = #"SongCell";
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:kCellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kCellIdentifier] autorelease];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.textLabel.font = [UIFont boldSystemFontOfSize:14];
}
Incident *incident = [fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = [NSString stringWithFormat:NSLocalizedString(#"#%d %#", #"#%d %#"), incident.title];
return cell;
}
- (void)tableView:(UITableView *)table didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[table deselectRowAtIndexPath:indexPath animated:YES];
self.detailController.incident = [fetchedResultsController objectAtIndexPath:indexPath];
[self.navigationController pushViewController:self.detailController animated:YES];
}
UPDATE: Here's the code where all instances of fetch are found.
- (Category *)categoryWithName:(NSString *)name {
NSTimeInterval before = [NSDate timeIntervalSinceReferenceDate];
#ifdef USE_CACHING
// check cache
CacheNode *cacheNode = [cache objectForKey:name];
if (cacheNode != nil) {
// cache hit, update access counter
cacheNode.accessCounter = accessCounter++;
Category *category = (Category *)[managedObjectContext objectWithID:cacheNode.objectID];
totalCacheHitCost += ([NSDate timeIntervalSinceReferenceDate] - before);
cacheHitCount++;
return category;
}
#endif
// cache missed, fetch from store - if not found in store there is no category object for the name and we must create one
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity:self.categoryEntityDescription];
NSPredicate *predicate = [self.categoryNamePredicateTemplate predicateWithSubstitutionVariables:[NSDictionary dictionaryWithObject:name forKey:kCategoryNameSubstitutionVariable]];
[fetchRequest setPredicate:predicate];
NSError *error = nil;
NSArray *fetchResults = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
[fetchRequest release];
NSAssert1(fetchResults != nil, #"Unhandled error executing fetch request in import thread: %#", [error localizedDescription]);
Category *category = nil;
if ([fetchResults count] > 0) {
// get category from fetch
category = [fetchResults objectAtIndex:0];
} else if ([fetchResults count] == 0) {
// category not in store, must create a new category object
category = [[Category alloc] initWithEntity:self.categoryEntityDescription insertIntoManagedObjectContext:managedObjectContext];
category.name = name;
[category autorelease];
}
#ifdef USE_CACHING
// add to cache
// first check to see if cache is full
if ([cache count] >= cacheSize) {
// evict least recently used (LRU) item from cache
NSUInteger oldestAccessCount = UINT_MAX;
NSString *key = nil, *keyOfOldestCacheNode = nil;
for (key in cache) {
CacheNode *tmpNode = [cache objectForKey:key];
if (tmpNode.accessCounter < oldestAccessCount) {
oldestAccessCount = tmpNode.accessCounter;
[keyOfOldestCacheNode release];
keyOfOldestCacheNode = [key retain];
}
}
// retain the cache node for reuse
cacheNode = [[cache objectForKey:keyOfOldestCacheNode] retain];
// remove from the cache
[cache removeObjectForKey:keyOfOldestCacheNode];
} else {
// create a new cache node
cacheNode = [[CacheNode alloc] init];
}
cacheNode.objectID = [category objectID];
cacheNode.accessCounter = accessCounter++;
[cache setObject:cacheNode forKey:name];
[cacheNode release];
#endif
totalCacheMissCost += ([NSDate timeIntervalSinceReferenceDate] - before);
cacheMissCount++;
return category;
}
And this one...
- (void)fetch {
NSError *error = nil;
BOOL success = [self.fetchedResultsController performFetch:&error];
NSAssert2(success, #"Unhandled error performing fetch at SongsViewController.m, line %d: %#", __LINE__, [error localizedDescription]);
[self.tableView reloadData];
}
- (NSFetchedResultsController *)fetchedResultsController {
if (fetchedResultsController == nil) {
NSFetchRequest *fetchRequest = [[[NSFetchRequest alloc] init] autorelease];
[fetchRequest setEntity:[NSEntityDescription entityForName:#"Song" inManagedObjectContext:managedObjectContext]];
NSArray *sortDescriptors = nil;
NSString *sectionNameKeyPath = nil;
if ([fetchSectioningControl selectedSegmentIndex] == 1) {
sortDescriptors = [NSArray arrayWithObjects:[[[NSSortDescriptor alloc] initWithKey:#"category.name" ascending:YES] autorelease], [[[NSSortDescriptor alloc] initWithKey:#"rank" ascending:YES] autorelease], nil];
sectionNameKeyPath = #"category.name";
} else {
sortDescriptors = [NSArray arrayWithObject:[[[NSSortDescriptor alloc] initWithKey:#"rank" ascending:YES] autorelease]];
}
[fetchRequest setSortDescriptors:sortDescriptors];
fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:sectionNameKeyPath cacheName:#"SongsCache"];
}
return fetchedResultsController;
}
your extra caching is probably a waste of cycles as Core Data performs its own caching internally. I am willing to bet you are slowing things down rather than speeding them up, not to mention the additional memory you are consuming.
Where are you setting categoryEntityDescription? That is now shown in the code you posted. It is probably nil.
Why are you retaining an NSEntityDescription?!? They are already in memory because of Core Data and retaining them is a waste which could lead to issues if Core Data wants to release it at some point.
update
Your caching is definitely not coming from Apple's code because they know that the cache is in Core Data.
As for the NSEntityDescription, again, do not retain the NSEntityDescription.
Are you 100% positive that the NSEntityDescription is not nil? Have you confirmed it in the debugger? Have you tested it with a freshly retrieved NSEntityDescription?
update
You need to learn to use the debugger as that will solve most of your coding issues. Put a breakpoint in this method and run your code in the debugger. Then when the execution stops on that break point you can inspect the values of the variables and learn what they are currently set to. That will confirm or deny your suspicions about what is and is not nil.
This error you are seeing happens when you fail to set the Entity in the NSFetchRequest which, based on your code, means that retained property is not being set before the code you have shown is being called.
Based on the code posted and the problem description, I suspect that the categoryEntityDescription property is returning nil.
I've seen this happen when the NSEntityDescription given to a fetch request is nil. The most likely cause of that is that you have a model entity that is named differently from the name you provided to entityForName. Barring that, it could be an error in configuration of your Core Data stack or a missing data model, but as a first step, I would recommend storing the result of entityForName in a local variable and breaking there to make sure it isn't nil.
Since you added the model file manually, is the .xcdatamodel file inside the Compile Sources step in your Target?
Go to the Targets entry in the Groups & Files pane in Xcode and click the disclosure triangle. Then click on the disclosure triangle for your app. Then check to see if it's in Compile Sources. If not, right click on Compile Sources and choose "Add -> Existing File..." and add it.
Edit based on update:
UPDATE: Here's the new code that
doesn't respond to the breakpoints.
- (UITableViewCell *)tableView:(UITableView *)table cellForRowAtIndexPath:(NSIndexPath *)indexPath
- (void)tableView:(UITableView *)table didSelectRowAtIndexPath:(NSIndexPath *)indexPath
Is your view controller set as the UITableViewDataSource/UITableViewDelegate for your UITableView? If not, these methods will not get called.

How can I maintain display order in UITableView using Core Data?

I'm having some trouble getting my Core Data entities to play nice and order when using an UITableView.
I've been through a number of tutorials and other questions here on StackOverflow, but there doesn't seem to be a clear or elegant way to do this - I'm really hoping I'm missing something.
I have a single Core Data entity that has an int16 attribute on it called "displayOrder". I use an NSFetchRequest that has been sorted on "displayOrder" to return the data for my UITableView. Everything but reordering is being respected. Here is my (inefficient) moveRowAtIndePath method:
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {
NSUInteger fromIndex = fromIndexPath.row;
NSUInteger toIndex = toIndexPath.row;
FFObject *affectedObject = [self.fetchedResultsController.fetchedObjects objectAtIndex:fromIndex];
affectedObject.displayOrderValue = toIndex;
[self FF_fetchResults];
for (NSUInteger i = 0; i < [self.fetchedResultsController.fetchedObjects count]; i++) {
FFObject *otherObject = [self.fetchedResultsController.fetchedObjects objectAtIndex:i];
NSLog(#"Updated %# / %# from %i to %i", otherObject.name, otherObject.state, otherObject.displayOrderValue, i);
otherObject.displayOrderValue = i;
}
[self FF_fetchResults];
}
Can anyone point me in the direction of a good bit of sample code, or see what I'm doing wrong? The tableview display updates OK, and I can see through my log messages that the displayOrder property is being updated. It's just not consistently saving and reloading, and something feels very "off" about this implementation (aside from the wasteful iteration of all of my FFObjects).
Thanks in advance for any advice you can lend.
I took a look at your code and this might work better:
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {
NSUInteger fromIndex = fromIndexPath.row;
NSUInteger toIndex = toIndexPath.row;
if (fromIndex == toIndex) {
return;
}
FFObject *affectedObject = [self.fetchedResultsController.fetchedObjects objectAtIndex:fromIndex];
affectedObject.displayOrderValue = toIndex;
NSUInteger start, end;
int delta;
if (fromIndex < toIndex) {
// move was down, need to shift up
delta = -1;
start = fromIndex + 1;
end = toIndex;
} else { // fromIndex > toIndex
// move was up, need to shift down
delta = 1;
start = toIndex;
end = fromIndex - 1;
}
for (NSUInteger i = start; i <= end; i++) {
FFObject *otherObject = [self.fetchedResultsController.fetchedObjects objectAtIndex:i];
NSLog(#"Updated %# / %# from %i to %i", otherObject.name, otherObject.state, otherObject.displayOrderValue, otherObject.displayOrderValue + delta);
otherObject.displayOrderValue += delta;
}
[self FF_fetchResults];
}
(This is intended as as comment on gerry3's answer above, but I am not yet able to comment on other users' questions and answers.)
A small improvement for gerry3's - very elegant - solution. If I'm not mistaken, the line
otherObject.displayOrderValue += delta;
will actually perform pointer arithmetic if displayOrderValue is not of primitive type. Which may not be what you want. Instead, to set the value of the entity, I propose:
otherObject.displayOrderValue = [NSNumber numberWithInt:[otherObject.displayOrderValue intValue] + delta];
This should update your entity property correctly and avoid any EXC_BAD_ACCESS crashes.
Here a full solution how to manage an indexed table with core data. Your attribute is called displayOrder, I call it index.
First of all, you better separate view controller and model. For this I use a model controller, which is the interface between the view and the model.
There are 3 cases you need to manage that the user can influence via the view controller.
Adding a new object
Deleting an existing object
Reorder objects.
The first two cases Adding and Deleting are pretty straightforward. Delete calls a routine called renewObjectIndicesUpwardsFromIndex in order to update the indices after the deleted object.
- (void)createObjectWithTitle:(NSString*)title {
FFObject* object = [FFObject insertIntoContext:self.managedObjectContext];
object.title = title;
object.index = [NSNumber numberWithInteger:[self numberTotalObjects]];
[self saveContext];
}
- (void)deleteObject:(FFObject*)anObject {
NSInteger objectIndex = [anObject.index integerValue];
[anObject deleteObject];
[self renewObjectIndicesUpwardsFromIndex:objectIndex];
[self saveContext];
}
- (void)renewObjectIndicesUpwardsFromIndex:(NSInteger)fromIndex {
NSFetchRequest* fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity:[NSEntityDescription entityForName:#"Object" inManagedObjectContext:self.managedObjectContext]];
NSPredicate* predicate = [NSPredicate predicateWithFormat:#"(index > %d)", fromIndex];
[fetchRequest setPredicate:predicate];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"index" ascending:YES];
NSArray *sortDescriptors = [NSArray arrayWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
NSError* fetchError = nil;
NSArray* objects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&fetchError];
NSInteger index = fromIndex;
for (FFObject* object in objects) {
object.index = [NSNumber numberWithInteger:index];
index += 1;
}
[self saveContext];
}
Before I come to the controller routines for the re-order, here the part in the view controller. I use a bool isModifyingOrder similar to this answer. Notice that the view controller calls two functions in the controller moveObjectOrderUp and moveObjectOrderDown. Depending on how you display the objects in the table view - newest first or newest last - you can switch them.
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath {
isModifyingOrder = YES;
NSUInteger fromIndex = sourceIndexPath.row;
NSUInteger toIndex = destinationIndexPath.row;
if (fromIndex == toIndex) {
return;
}
FFObject *affectedObject = [self.fetchedResultsController.fetchedObjects objectAtIndex:fromIndex];
NSInteger delta;
if (fromIndex < toIndex) {
delta = toIndex - fromIndex;
NSLog(#"Moved down by %lu cells", delta);
[self.objectController moveObjectOrderUp:affectedObject by:delta];
} else {
delta = fromIndex - toIndex;
NSLog(#"Moved up by %lu cells", delta);
[self.objectController moveObjectOrderDown:affectedObject by:delta];
}
isModifyingOrder = NO;
}
And here the part in the controller. This can be written nicer, but for understanding this is maybe best.
- (void)moveObjectOrderUp:(FFObject*)affectedObject by:(NSInteger)delta {
NSInteger fromIndex = [affectedObject.index integerValue] - delta;
NSInteger toIndex = [affectedObject.index integerValue];
if (fromIndex < 1) {
return;
}
NSFetchRequest* fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity:[NSEntityDescription entityForName:#"Object" inManagedObjectContext:self.managedObjectContext]];
NSPredicate* predicate = [NSPredicate predicateWithFormat:#"(index >= %d) AND (index < %d)", fromIndex, toIndex];
[fetchRequest setPredicate:predicate];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"index" ascending:YES];
NSArray *sortDescriptors = [NSArray arrayWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
NSError* fetchError = nil;
NSArray* objects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&fetchError];
for (FFObject* object in objects) {
NSInteger newIndex = [object.index integerValue] + 1;
object.index = [NSNumber numberWithInteger:newIndex];
}
affectedObject.index = [NSNumber numberWithInteger:fromIndex];
[self saveContext];
}
- (void)moveObjectOrderDown:(FFObject*)affectedObject by:(NSInteger)delta {
NSInteger fromIndex = [affectedObject.index integerValue];
NSInteger toIndex = [affectedObject.index integerValue] + delta;
NSFetchRequest* fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity:[NSEntityDescription entityForName:#"Object" inManagedObjectContext:self.managedObjectContext]];
NSPredicate* predicate = [NSPredicate predicateWithFormat:#"(index > %d) AND (index <= %d)", fromIndex, toIndex];
[fetchRequest setPredicate:predicate];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"index" ascending:YES];
NSArray *sortDescriptors = [NSArray arrayWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
NSError* fetchError = nil;
NSArray* objects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&fetchError];
for (FFObject* object in objects)
{
NSInteger newIndex = [object.index integerValue] - 1;
object.index = [NSNumber numberWithInteger:newIndex];
}
affectedObject.index = [NSNumber numberWithInteger:toIndex];
[self saveContext];
}
Don't forget to use a second BOOL in your view controller for the delete action to prevent the move notification to do anything. I call it isDeleting and put it here.
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath {
if (isModifyingOrder) return;
...
switch(type) {
...
case NSFetchedResultsChangeMove:
if (isDeleting == false) {
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:localIndexPath] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:localNewIndexPath]withRowAnimation:UITableViewRowAnimationFade];
}
break;
...
}
}
I think that:
affectedObject.displayOrderValue = toIndex;
must be placed after:
for (NSUInteger i = start; i <= end; i++) {
FFObject *otherObject = [self.fetchedResultsController.fetchedObjects objectAtIndex:i];
NSLog(#"Updated %# / %# from %i to %i", otherObject.name, otherObject.state, otherObject.displayOrderValue, otherObject.displayOrderValue + delta);
otherObject.displayOrderValue += delta;
}
and before:
[self FF_fetchResults];
The answers above (as far as I can tell) only work if cell is being moved up or down in the same section. For this approach to be valid, one would have to prevent the user from moving between sections. (Using the canMoveRowAt indexPath: IndexPath -> Bool tableView delegate method).
To maintain display order in a UITableView when moving a cell within a section, or to a different section, here is code I stole verbatim from https://github.com/MrAlek/Swift-NSFetchedResultsController-Trickery/blob/ceac7937a3b20f78d7268274b18eef4845917090/CoreDataTrickerySwift/ToDoViewController.swift
This code uses a ToDo as the NSManagedObject subclass.
Here is the meat of the logic:
func updateInternalOrderForToDo(_ toDo: ToDo, sourceIndexPath: IndexPath, destinationIndexPath: IndexPath) {
// Update internal order to reflect new position
// First get all toDos, in sorted order
var sortedToDos = fetchedResultsController.fetchedObjects!
sortedToDos = sortedToDos.filter() {$0 != toDo} // Remove current toDo
// Insert toDo at new place in array
var sortedIndex = destinationIndexPath.row
for sectionIndex in 0..<destinationIndexPath.section {
sortedIndex += toDoListController.sections[sectionIndex].numberOfObjects
if sectionIndex == sourceIndexPath.section {
sortedIndex -= 1 // Remember, controller still thinks this toDo is in the old section
}
}
sortedToDos.insert(toDo, at: sortedIndex)
// Regenerate internal order for all toDos
for (index, toDo) in sortedToDos.enumerated() {
toDo.metaData.internalOrder = sortedToDos.count-index
}
}
}
There is some tweaking of the moveRowAt depending on whether one is directly updating a cell, moving to a different section, and or using Snapshots with DiffableData source. A key concept is to temporarily disable NSFetchedResultsController delegate calls to update the table. This is covered elsewhere, including a link referenced above. The code below is not intended to by copy / pasted, but just to illustrate some of the considerations of what should be handled in moveRowAt call
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
if sourceIndexPath == destinationIndexPath {
return
}
// Don't let fetched results controller affect table view
fetchControllerDelegate.ignoreNextUpdates = true
// Trust that we will get a toDo back
let toDo = toDoListController.toDoAtIndexPath(sourceIndexPath)!
//manually update managedObject properties as necessary to reflect inclusion in the new section.
if sourceIndexPath.section != destinationIndexPath.section {
//handle changes to managed objects propertie(s) here
}
// Table view is in inconsistent state, update the cell. If you are using a DiffableDataSource skip this and see below
if let cell = tableView.cellForRow(at: destinationIndexPath) {
self.configureCell(cell, toDo: toDo)
}
//see method implementation above
updateInternalOrderForToDo(toDo, sourceIndexPath: sourceIndexPath, destinationIndexPath: destinationIndexPath)
// Save
try! toDo.managedObjectContext!.save()
//if you are using DiffableDataSource, call apply(newSnapshot, animatingDifferences: animated) on your UIDifffableDataSource object after saving the moc. make sure ignoreNextUpdates bool is still set to true before calling save()
}