Updating Fetchedresultscontroller for Predicate Set by Uisearchbar

Updating fetchedResultsController for predicate set by UISearchBar

This is not-so-swift-pseudo-code... it most assuredly will not compile with any swift compiler.

Your FRC is not using a cache, so there is no cache to be deleted, and we can just assign the predicate to the existing FRC fetch request.

You may not want to do a complete new fetch on every character change in the search bar, or you may want to only do the search on the first character, and use subsets for subsequent characters.

// called when text changes (including clear)
internal func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
var predicate:NSPredicate = nil
if searchBar.text.length != 0 {
predicate = NSPredicate(format: "(songDescription contains [cd] %@) || (songStar contains[cd] %@)", searchBar.text!, searchBar.text!)
}
fetchedResultsController.fetchRequest.predicate = predicate
fetchedResultsController.performFetch()
tableView.reloadData()
}

// called when cancel button pressed
internal func searchBarCancelButtonClicked(searchBar: UISearchBar) {
searchBar.resignFirstResponder()
searchBar.text = ""
fetchedResultsController.fetchRequest.predicate = nil
fetchedResultsController.performFetch()
tableView.reloadData()
}

NSFetchedResultsController - Change in Runtime variable for Predicate

I have tried with following code snippet and it works for me:

[NSFetchedResultsController deleteCacheWithName:nil];  
[self.fetchedResultsController.fetchRequest setPredicate:predicate]; // set your new predicate

NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error]) {
NSLog(@"%@, %@", error, [error userInfo]);
abort();
}

Swift Using NSFetchedResultsController and UISearchBarDelegate

From your code, I assume you want to use the same table view to display the results. So you just need to update your FRC with a new filter based on the search term.

Store the search term in a variable. In the FRC factory function, include the predicate, something like this:

request.predicate = searchText?.characters.count > 0 ?
NSPredicate(format:"title contains[cd] %@", searchText!) : nil

When the text changes, reset the FRC and reload.

fetchedResultsController = nil
tableView.reloadData()

If you have additional filters, such as scope buttons, add additional terms to the predicate.

NSFetchedResultController and UISearchBar implementation

If you modify your fetch request (such as by changing the predicate), you need to create a new fetched results controller.

Alternatively, you could access your fetched objects as an array (using the fetchedObjects property of the fetched results controller) and filter that array based on the search criteria. This has the added benefit of aligning more with Apple's sample code for table search (since they use an array to back the table view).

How to filter NSFetchedResultsController (CoreData) with UISearchDisplayController/UISearchBar

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

FetchedResultsController Using Predicate to Exclude Results Contained in a Set

Thanks to the post here: CoreData Predicate get every sentence that contains any word in array
, I was finally able to solve my problem. My current working code is here:

func configureFetchedResultsController() {
let context = databaseController.getContext()
let enginesFetchRequest = NSFetchRequest<Engine>(entityName: CoreData.engine)
let lowPowerEngines = ["A", "B", "C"]

var predicate = NSPredicate()
if currentStage == 1 && stages! == 1 {
predicate = NSPredicate(format: "engineType == %@", EngineType.singleStage.rawValue)
} else if currentStage < stages! {
predicate = NSPredicate(format: "engineType == %@", EngineType.boosterStage.rawValue)
} else {
predicate = NSPredicate(format: "engineType == %@", EngineType.upperStage.rawValue)
}

var predicateArray:[NSPredicate] = [
predicate
]

if !dPlusEngineIAP {
let predicates = lowPowerEngines.map {
NSPredicate(format: "engineDesignation CONTAINS %@", $0)
}
let predicate = NSCompoundPredicate(orPredicateWithSubpredicates: predicates)
predicateArray.append(predicate)
}

let compoundPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicateArray)
enginesFetchRequest.predicate = compoundPredicate

let primarySortDescriptor = NSSortDescriptor(key: CoreData.isMadeByName, ascending: true)
let secondarySortDescriptor = NSSortDescriptor(key: CoreData.engineDesignation, ascending: true)
enginesFetchRequest.sortDescriptors = [primarySortDescriptor, secondarySortDescriptor]

self.fetchedResultsController = NSFetchedResultsController<Engine>(
fetchRequest: enginesFetchRequest,
managedObjectContext: context,
sectionNameKeyPath: CoreData.isMadeByName,
cacheName: nil)

self.fetchedResultsController.delegate = self

}

The solution to dealing with each item in the array was to make a compound predicate using the map function. Then adding the compound predicate to the fetchedResultsController. That allows me to compare the engine to see if it is in lowPowerEngines. Map makes a separate predicate for each item in lowPowerEngines. I could even change it programmatically on the fly. I really hope this helps someone as there is no central place to come up with these sort of tricks. When I collect enough of them, I will do a master post.

Swift UISearchController wired up in Core Data Project, app runs, but search not updating

The problems lie in your tableView datasource methods: numberOfSectionsInTableview:, tableView:numberOfRowsInSection:, and tableView:cellForRowAtIndexPath:. You need each of those methods to return different results if the searchPredicate is not nil - much like your tableView:commitEditingStyle: method does. I would make filteredObjects an instance property (defined at the start of the class) so that all those methods can access it:

var filteredObjects : [Note]? = nil

Now, when the search text changes, you want to rebuild the filteredObjects array. So in updateSearchResultsForSearchController, add a line to recompute it based on the new predicate:

    if let searchText = searchText {
searchPredicate = NSPredicate(format: "noteBody contains[c] %@", searchText)
filteredObjects = self.fetchedResultsController.fetchedObjects?.filter() {
return self.searchPredicate!.evaluateWithObject($0)
} as [Note]?
self.tableView.reloadData()
println(searchPredicate)
}

I would also recommend (for simplicity) that when you activate the search, the results are displayed all in one section (otherwise you have to work out how many sections your filtered results fall into - possible but tiresome):

override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
if searchPredicate == nil {
return self.fetchedResultsController.sections?.count ?? 0
} else {
return 1
}
}

Next, if the searchPredicate is not nil, the number of rows in the section will be the count of filteredObjects:

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if self.searchPredicate == nil {
let sectionInfo = self.fetchedResultsController.sections![section] as! NSFetchedResultsSectionInfo
return sectionInfo.numberOfObjects
} else {
return filteredObjects?.count ?? 0
}
}

Finally, if the searchPredicate is not nil, you need to build the cell using the filteredObjects data, rather than the fetchedResultsController:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! UITableViewCell
if searchPredicate == nil {
self.configureCell(cell, atIndexPath: indexPath)
return cell
} else {
// configure the cell based on filteredObjects data
...
return cell
}
}

Not sure what labels etc you have in your cells, so I leave it to you to sort that bit.

Is there a way to modify fetched results with a predicate after they are initialized?

Is there a way to modify fetched results with a predicate after they
are initialized?

Well... no, not in the way you try to do this, and even if you'd try to create it with NSFetchRequest instance, which is reference, and allows to change predicate later, that wouldn't work, because SwiftUI's FetchRequest stores copy of provided fetch request (or creates own with provided parameters)... so, no. But...

You can break apart view providing fetch request parameters with view constructing fetch request and showing result.

Here is a demo of approach (important part of it) which gives you possibility to get results with different dynamically changed predicates:

struct MasterView: View {
@State var predicate: NSPredicate? = nil
var body: some View {
VStack {
Button(action: { // button just for demo
self.predicate = NSPredicate(format: "title contains[c] %@", "h")
}, label: { Text("Filter") })
ResultView(predicate: self.predicate)
}
}
}

struct ResultView: View {

@FetchRequest
var events: FetchedResults<Event>

@Environment(\.managedObjectContext)
var viewContext

init(predicate: NSPredicate?) {
let request: NSFetchRequest<Event> = Event.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(keyPath: \Event.timestamp, ascending: true)]
if let predicate = predicate {
request.predicate = predicate
}
_events = FetchRequest<Event>(fetchRequest: request)
}

var body: some View {
List {
ForEach(events, id: \.self) { event in
...


Related Topics



Leave a reply



Submit