How to filter NSFetchedResultsController (CoreData) with UISearchDisplayController/UISearchBar
Solution 1
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];
}
Solution 2
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;
}
Solution 3
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:
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.
Solution 4
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
Solution 5
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!
Related videos on Youtube
jschmidt
I am a former developer and project manager now turned attorney.
Updated on July 08, 2022Comments
-
jschmidt almost 2 years
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!
-
DetartrateD almost 13 years@Brent, perfect solution, worked a treat for me!
-
jschmidt over 13 yearsVery interesting! I'll give that a try.
-
Brent Priddy over 13 yearsThis seems to work but it fails when your table is populated by the FRC, the searchTableView is a different table from the main table view you use. The FRC delegate methods puke all over the place when on a device with low memory when searching and the main tableView wants to reload cells.
-
jschmidt over 13 yearsThat seems to work beautifully! Thanks, Brent! I particularly like the fetchedResultsControllerForTableView: method. That makes it very easy!
-
RyeMAC3 almost 13 yearsAnyone have a link to a project template for this? I'm finding it really hard figuring out what goes where. It would be very nice to have a working template as a reference.
-
kervich over 12 years@Brent, You should check if that's the search tableview that needs changes in FRC delegate methods - if you do and update the right table in the FRC and UITableView delegates' methods then everything should be ok when using FRC for both the main tableview and the search tableview.
-
Brent Priddy over 12 years@kervich I believe you are describing my answer above or are you saying you can do it with only one FRC?
-
Daniel Amitay over 12 yearsRidiculously good code. As jschmidt said, the custom "fetchedResultsControllerForTableView:" method really simplifies the entire process.
-
jschmidt over 12 yearsBrent. You are the man. But, here's a new challenge for you. Implementation of this code using background processing. I have done some minor multi-threading of other parts of my app, but this is tough (for me at least). I think it would add a nicer user experience. Challenge accepted?
-
Brent Priddy over 12 yearsYou really should ask another question and let others who have worked on this answer you (I have not done anything with background pre-caching for CoreData, I have not run into an issue that requires it)
-
kervich over 12 years@Brent, I'm just saying that you can do it with one FRC, provided that the delegate methods are aware of which UITableView they are working on. In fact, I use one FRC to show a list and to filter it simply by changing the NSPredicate when the search bar contents is modified.
-
Admin over 12 yearsBrent, do you have a version of this code that has just a searchFRzc where the initial tableview is loaded by am a separate array?
-
Brent Priddy over 12 years@Faisal, I don't but you can use the same concept. Select your data source based on the table view that is querying for the data. Either use the FRC or the array.
-
ma11hew28 over 12 years@BrentPriddy Thanks! I refactored your code to Modify the Fetch Request instead of setting the
searchFetchedResultsController
tonil
every time the search text changes. -
Sanjiv Jivan about 12 yearsThe above code is missing calls to super for various overridden methods and that missing call to [super loadView] in particular causes issues when using Storyboards - the loadView method gets called multiple times and results in a corrupt view.
-
acecapades about 12 yearsI have a UIManagedDocument and I want to use its managedObjectContext instead of having a designated property for the context, how do I integrate that? Thanks
-
Vladimir Stazhilov over 11 yearscode seems to be nice but filters objects only a single time.... something is wrong at least for me...
-
Vladimir Stazhilov over 11 yearsyet I don't understand, the example above very good, but incomplete, but your recommendation should work, but it doesn't...
-
Brent Priddy over 11 years@VovaStajilov maybe something is wrong with the filterContentForSearchText:scope: not getting called to kill the fetched results controller
-
rwyland over 11 yearsYou could just check the active table view instead of using a BOOL:
if ( [self.tableView isEqual:self.searchDisplayController.searchResultsTableView] ) { ... }
-
Kai Engelhardt over 11 years@BrentPriddy +1 for being so awesome :D
-
abisson over 11 yearsI am having troubling showing the filtered data in the self.tableView. It seems like the fetchedResultsControllerForTableView function always returns me the fetchedResultsController, and therefore, I am not showing the "searched" content. I have only one tableView (which is self.tableView). Is that good? Thanks!
-
amb over 11 yearsIn your
cellForRowAtIndexPath
, shouldn't you get the cell fromself.tableView
like somebody pointed in this SO question? If you don't do this, the custom cell is not displayed. -
Brent Priddy over 11 yearsabisson and amb, you have to use the tableView passed into the functions and not the self.tableView. Since there are 2 tableviews you have to rely on the callbacks providing the correct table.
-
giff over 10 years@rwyland - My testing shows that self.tableview is not set to the searchdisplaycontroller.searchresultstableview when search is active. These would never be equal.
-
Kavya Indi over 10 years@Brent Priddy: For some reason, your code isn't working when you have selected any filtered object and update/edit it then it does not work. Can you please guide me in this direction.
-
Guto Araujo over 10 yearsThis solution worked well for me and it's much simpler. Thanks! I'd only suggest tweaking 'if (self.searchString)' to 'if (self.searchString.length). This prevents it from crashing if you click on the table view after starting a search and deleting the string from the search bar.
-
motionpotion over 10 years@Brent Priddy: What modifications would be required for this to work with a UITableView on a UIViewController rather than as it is currently working with a UITableViewController?
-
Brent Priddy over 10 years@motionpotion: take all tableview methods and implement them whwever you are going to work with the UIViewController
-
Brent Priddy over 10 years@Kavya Indi: The search tableview as well as the main one should get updated when you change the entry enough to change the key and sorting material. try to see why you are not getting an update in the controller:didChange* messages.
-
Saqib Saud about 10 yearsI implemented first FRC using two FRC's and it worked like a charm. After getting some understanding, I used single FRC in second implementation, it also worked like a charm. :)
-
Joseph almost 9 yearsSince
UISearchDisplayController
has been deprecated in iOS 8, has anyone found a more up-to-date example? -
trapper almost 8 yearsIf you are recreating a secondary fetchedResultsController each time the search text changes then why bother to keep the primary one around at all? Just use a single controller and avoid all this overly complicated code.