Hi I am trying to implement a search bar function for a table view using core data and NSFetchedResults Controller.
Quite a number of answers on SO suggest using a predicate for search using something like the following code:
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
if ([searchText length] == 0) {
_fetchedResultsController = nil;
}
else {
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"name contains[cd] %#", searchText];
[[_fetchedResultsController fetchRequest] setPredicate:predicate];
[[_fetchedResultsController fetchRequest] setFetchLimit:50];
}
NSError *error;
if (![[self fetchedResultsController] performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
[[self tableView] reloadData];
}
I have also tried variations of this. In every case, however, I'm having the same problem.
First, when you search, it says no results. Also if you type in a few letters and hit the x, you get a grayed out screen and if you click again, you get the normal table view.
However, after typing in a letter and seeing "No Results" if you click cancel you get the results you should have gotten. Once that happens you cannot get back to the full tableview without rebuilding the project.
The only idea I have so far is it might have something to do with _fetchedResultsController vs fetchedResultsController (the property in this source file is ftchedResultsController without the underscore) but changing those only throws error messages.
Thank you for any suggestions on what could be causing this.
You have two table views, one from the search and the main one from the main table. You should disable the search controller's table view if you want to keep showing your main table view with the filter implied by the search.
self.searchResultsController.active = NO;
You could set this every time the search bar would otherwise set this to YES, like in textDidChange. In your FRC getter, you check for any string in the search bar and add a predicate if necessary. In textDidChange you nil out the FRC and reloadData. That's it.
Related
There are a lot of questions on this topic but perhaps all too specific and none too concise.
I have a NSFetchedResultsController. It initially fetches the data I need. When I update the data model which would affect the results of the NSPredicate of the NSFetchRequest, the content does not update.
More concretely, I have a Permissions model. There are data objects that are assigned permissions, then there are users who have a subset of these permissions, though the data object's Permission is not the same as the User's permission; they do share the same controlKey.
So, the NSPredicate is:
[NSPredicate predicateWithFormat:#"controlKey IN %#", [[User currentUser].permissions valueForKey:#"controlKey"]];
The problem is, if I create and add a permission, the content does not update when I save the NSManagedObjectContext (yes, I'm observing it. No, I'm not using a cache so nothing to delete).
I'm pretty convinced it's because of the predicate. It's still the same array as initially, and doesn't get updated.
My question is, how do I write a predicate that gets me what I want but still remains "dynamic" ? It would be nice to have UITableView animations as this object is added.
Here is what you need to do. When detecting the change,
self.fetchedResultsController.predicate = //new predicate
[self.fetchedResultsController performFetch:nil];
[self.tableView reloadData];
Or sometimes it is necessary to wipe the FRC clean.
_predicate = // new predicate, put it into an ivar
self.fetchedResultsController = nil;
[self.tableView reloadData]; // lazily re-instantiate FRC with _predicate
This could be expensive, but in order to get animations, you could try replacing reloadData with the following:
[self.tableView beginUpdates];
[self.tableView endUpdates];
You query the control keys from a set of permissions and pass them to the predicate. The predicate itself isn't dynamic anyway, it is only the result of the fetch which is dynamic. So, you can't do exactly what you want.
Instead, you should be observing the permissions change for the user, creating a new predicate, updating the FRC and re-executing it (a requirement after you change the predicate, because it is expected to be static).
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();
}
In my app I have a table view controller with two buttons. One adds an item, and the other edits the table view. When I add an item, the console says that it has added an item, but it's not until i restart the simulator to see that items are being added to the table view. When I add an item I make sure to reload the tableview like so:
- (IBAction)addNewItem:(id)sender
{
NSManagedObjectContext *context = [self managedObjectContext];
Item *itemData = [NSEntityDescription insertNewObjectForEntityForName:#"Item" inManagedObjectContext:context];
[itemData setValue:userText.text forKey:#"name"];
NSError *error;
if (![context save:&error])
{
NSLog(#"Couldnt find the save %#", error.localizedDescription);
}
else
{
NSLog(#"It saved properly");
}
[self.tableView reloadData];
}
I just don't understand what the problem could be...
All help is appreciated,
Thanks in advance
The tableView keep to show the old fetch result after the new item has been added. You need to fetch the data again after item has been added and then call the [self.tableView reloadData] method.
Let's say I load in 1,000 objects via Core Data, and each of them has a user-settable Favorite boolean. It defaults to NO for all objects, but the user can paw through at will, setting it to YES for as many as they like. I want a button in my Settings page to reset that Favorite status to NO for every single object.
What's the best way to do that? Can I iterate through every instance of that Entity somehow, to set it back? (Incidentally, is 'instance' the right word to refer to objects of a certain entity?) Is there a better approach here? I don't want to reload the data from its initial source, since other things in there may have changed: it's not a total reset, just a 'Mass Unfavourite' option.
Okay, I've gotten it to work, but it requires a restart of the app for some reason. I'm doing this:
- (IBAction) resetFavourites: (id) sender
{
NSFetchRequest *fetch = [[NSFetchRequest alloc] init];
[fetch setEntity: [NSEntityDescription entityForName: #"Quote" inManagedObjectContext: [[FQCoreDataController sharedCoreDataController] managedObjectContext]]];
NSArray *results = [[[FQCoreDataController sharedCoreDataController] managedObjectContext] executeFetchRequest: fetch error: nil];
for (Quote *quote in results) {
[quote setIsFavourite: [NSNumber numberWithBool: NO]];
}
NSError *error;
if (![self.fetchedResultsController performFetch: &error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
[self.tableView reloadData];
}
This works fine if I close and re-open the app, but it isn't reflected immediately. Isn't that what the reloadData should do, cause an immediate refresh? Am I missing something there?
I have a NSFetchedResultsController to get the data for an UITableView. During the creation of the NSFetchedResultsController I create a NSPredicate that filters the data with an external condition. What's the proper way to refetch the data? Just nil'ing the my __fetchedResultsController and recreating it seems a little bit brutal.
Thanks!
The initWithFetchRequest:managedObjectContext:sectionNameKeyPath:cacheName: documentation states:
Important: You must not modify fetchRequest after invoking this
method. For example, you must not change its predicate or the sort
orderings.
So you have to recreate the FRC (and call reloadData on the table view) when the predicate changes.
EDIT:
The same "NSFetchedResultsController Class References" also states here:
Modifying the Fetch Request
You cannot simply change the fetch request
to modify the results. If you want to change the fetch request, you
must:
If you are using a cache, delete it (using deleteCacheWithName:).
Typically you should not use a cache if you are changing the fetch
request.
Change the fetch request.
Invoke performFetch:.
So my first answer above is probably wrong. If I understand it correctly:
You cannot modify the fetch request assigned to the FRC.
But you can build a new fetch request (with a new predicate) and assign that to the FRC (controller.fetchRequest = newFetchRequest) if you follow the three steps above.
This means that you don't have to recreate the FRC itself.
(I hope that I got it right now :-)
As indicated in my comment above I figured it out. What you need to do is save a reference to the NSFetchRequest and manipulate that straight away when needed. As a second step, you need to tell your NSFetchedResultsController to fetch it's data again.
I did this by adding two new methods to the default NSFetchedResultsController "stack":
- (void)configureFetchRequest {
NSObject *myExternalDependency = …;
if (!__fetchRequest) {
__fetchRequest = [[NSFetchRequest alloc] init];
}
NSEntityDescription *entity = [NSEntityDescription entityForName:#"EntityName" inManagedObjectContext:self.managedObjectContext];
[__fetchRequest setEntity:entity];
NSPredicate *filter = [NSPredicate predicateWithFormat:#"someValue == %#", [myExternalDependency someProperty]];
[__fetchRequest setPredicate:filter];
}
- (void)performFetch {
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}
So what I basically do now is call these two methods on creation of __fetchedResultsController (which I do initialize using __fetchRequest, of course) and every time my external dependency changes. That's it.
I want to add a google instant search bar in my iOS application like the one in iOS Safari and I couldn't find it anywhere. Can you guys help?
Thanks in advance.
I assume that you are using UISearchBar.
You can use your code from
- (void) searchBarSearchButtonClicked:(UISearchBar *)theSearchBar
in method
- (void) searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
Just keep in mind, that you dont need to resignFirstResponder in searchBar:textDidChange.
For example, in this methods can be:
NSError *error = nil;
// We use an NSPredicate combined with the fetchedResultsController to perform the search
if (self.mySearchBar.text !=nil)
{
if ([searchCriteria isEqualToString:#"artist"])
{
NSPredicate *predicate =[NSPredicate predicateWithFormat:#"artist contains[cd] %#", self.mySearchBar.text];
[fetchedResultsController.fetchRequest setPredicate:predicate];
}
}
else
{
NSPredicate *predicate =[NSPredicate predicateWithFormat:#"All"];
[fetchedResultsController.fetchRequest setPredicate:predicate];
}
if (![[self fetchedResultsController] performFetch:&error])
{
// Handle error
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
exit(-1); // Fail
}
// this array is just used to tell the table view how many rows to show
fetchedObjects = fetchedResultsController.fetchedObjects;
// dismiss the search keyboard
[mySearchBar resignFirstResponder];
// reload the table view
[myTableView reloadData];
}
Feel free to +1 this answer, if that helps for someone like me, they come from google search results to this post :)
brush51