How to filter NSFetchedResultsController (CoreData) with UISearchDisplayController/UISearchBar

54,759

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:

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.

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!

Share:
54,759

Related videos on Youtube

jschmidt
Author by

jschmidt

I am a former developer and project manager now turned attorney.

Updated on July 08, 2022

Comments

  • jschmidt
    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:

    1. 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?

    2. 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?

    3. 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
      DetartrateD almost 13 years
      @Brent, perfect solution, worked a treat for me!
  • jschmidt
    jschmidt over 13 years
    Very interesting! I'll give that a try.
  • Brent Priddy
    Brent Priddy over 13 years
    This 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
    jschmidt over 13 years
    That seems to work beautifully! Thanks, Brent! I particularly like the fetchedResultsControllerForTableView: method. That makes it very easy!
  • RyeMAC3
    RyeMAC3 almost 13 years
    Anyone 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
    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
    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
    Daniel Amitay over 12 years
    Ridiculously good code. As jschmidt said, the custom "fetchedResultsControllerForTableView:" method really simplifies the entire process.
  • jschmidt
    jschmidt over 12 years
    Brent. 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
    Brent Priddy over 12 years
    You 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
    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
    Admin over 12 years
    Brent, 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
    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
    ma11hew28 over 12 years
    @BrentPriddy Thanks! I refactored your code to Modify the Fetch Request instead of setting the searchFetchedResultsController to nil every time the search text changes.
  • Sanjiv Jivan
    Sanjiv Jivan about 12 years
    The 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
    acecapades about 12 years
    I 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
    Vladimir Stazhilov over 11 years
    code seems to be nice but filters objects only a single time.... something is wrong at least for me...
  • Vladimir Stazhilov
    Vladimir Stazhilov over 11 years
    yet I don't understand, the example above very good, but incomplete, but your recommendation should work, but it doesn't...
  • Brent Priddy
    Brent Priddy over 11 years
    @VovaStajilov maybe something is wrong with the filterContentForSearchText:scope: not getting called to kill the fetched results controller
  • rwyland
    rwyland over 11 years
    You could just check the active table view instead of using a BOOL: if ( [self.tableView isEqual:self.searchDisplayController.searchResultsTableView] ) { ... }
  • Kai Engelhardt
    Kai Engelhardt over 11 years
    @BrentPriddy +1 for being so awesome :D
  • abisson
    abisson over 11 years
    I 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
    amb over 11 years
    In your cellForRowAtIndexPath, shouldn't you get the cell from self.tableView like somebody pointed in this SO question? If you don't do this, the custom cell is not displayed.
  • Brent Priddy
    Brent Priddy over 11 years
    abisson 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
    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
    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
    Guto Araujo over 10 years
    This 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
    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
    Brent Priddy over 10 years
    @motionpotion: take all tableview methods and implement them whwever you are going to work with the UIViewController
  • Brent Priddy
    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
    Saqib Saud about 10 years
    I 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
    Joseph almost 9 years
    Since UISearchDisplayController has been deprecated in iOS 8, has anyone found a more up-to-date example?
  • trapper
    trapper almost 8 years
    If 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.