How do I search for results in this table view? - ios

I want to be able to add a Search feature to my table view. The table lists various store locations. I want the user to be able to filter by city. I have been thinking of adding a dropdown list somehow, perhaps in a tableviewcell to create the filter. Has anyone done this who could help me out?
I already have a search bar control in the table view but it only searches by one field of the records. How do I decide or select which field the search bar actually looks for in my data? This is my code:
#pragma mark Content Filtering
-(void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope {
// Update the filtered array based on the search text and scope.
// Remove all objects from the filtered search array
[self.filteredResultsArray removeAllObjects];
// Filter the array using NSPredicate
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"SELF.name contains[c] %#",searchText];
[self.filteredResultsArray = [NSMutableArray arrayWithArray:[self.dates filteredArrayUsingPredicate:predicate]] retain];
}
#pragma mark - UISearchDisplayController Delegate Methods
-(BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString {
// Tells the table data source to reload when text changes
[self filterContentForSearchText:searchString scope:
[[self.searchDisplayController.searchBar scopeButtonTitles] objectAtIndex:[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 {
// Tells the table data source to reload when scope bar selection changes
[self filterContentForSearchText:self.searchDisplayController.searchBar.text scope:
[[self.searchDisplayController.searchBar scopeButtonTitles] objectAtIndex:searchOption]];
// Return YES to cause the search result table view to be reloaded.
return YES;
}
my self.dates array is populated like so:
- (void)loadRecordsFromCoreData {
[self.managedObjectContext performBlockAndWait:^{
[self.managedObjectContext reset];
NSError *error = nil;
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:self.entityName];
[request setSortDescriptors:[NSArray arrayWithObject:
[NSSortDescriptor sortDescriptorWithKey:#"date" ascending:YES]]];
self.dates = [self.managedObjectContext executeFetchRequest:request error:&error];
}];
}
Does it have to do with the initWithEntityName...

NSPredicate *predicate = [NSPredicate predicateWithFormat:#"SELF.name contains[c] %#",searchText];
That predicate determines which field is being searched in your data - the name field in this case. If you want to search on a different field just change the SELF.name to SELF.city or whatever the other field is. If you want to search on multiple fields or use additional logic in the filter, check out Apple's NSPredicate Programming Guide.

Related

Adding a Search Bar to a Table View with Core Data

I have an app that saves data to a Core Data sql then view it in a table view.
The data model:
Entity: Fruits
Attributes: name, picture
So in the table view, cell.textLabel.text = Fruits.name
Each cell has a segue with a view that shows Fruits.picture.
I want to add a search bar to search in Fruits names.
I followed this tutorial: http://www.appcoda.com/search-bar-tutorial-ios7/
But the problem I had is in filterContentForSearchText
- (void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope
{
NSPredicate *resultPredicate = [NSPredicate predicateWithFormat:#"name contains[c] %#", searchText];
searchResults = [recipes filteredArrayUsingPredicate:resultPredicate];
}
I'm using core data not an array for storing data. And I don't know how to filter it so I can display searchResults in table cells and use it in prepareForSegue.
Since you're using core data and not an array of Fruits you should filter your data this way:
- (void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope
{
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"Fruits" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"name contains[c] %#", searchText];
[fetchRequest setPredicate:predicate];
NSError *error;
NSArray* searchResults = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
}
To show the picture in a second viewController you should put something like this in you prepareForSegue method:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
Fruit *selectedItem;
if (self.searchDisplayController.active) {
selectedItem = [searchResults objectAtIndex:indexPath.row];
} else {
selectedItem = [fruitsList objectAtIndex:indexPath.row];
}
DestinationViewController *destination = segue.destinationViewController;
destination.fruit = selectedItem;
}
Where fruitsList is an array with all the fruit objects (The one used to show the fruits without any filter), and DestinationViewController is the the controller that will show the picture.
First, why is your entity name in the plural? That is confusing. You should rename your entity Fruit.
Second, how come your variable names start with capital letters? This is again confusing because they can be mistaken for class names. You should rename your variables fruit, such as in fruit.name.
Third, you should use a NSFetchedResultsController to populate your table view. You could have a separate one just for the search or filter in memory. Both are preferable to doing a manual fetch in each call to filterContentForSearchText. Your filter code looks fine (it is the in-memory version). However, I do not see where you reload the data of your search results table view.
Fourth, the search is completely unrelated to the segue problem. Each cell should be associated with a particular Fruit instance. (Either custom cells with a #property of type Fruit, or use the fetched results controller objectAtIndexPath.) In prepareForSegue you just assign that fruit to the detail view controller (which should have an appropriate #property set up).

NSFetchedResultsController delegate method calls on predicate change

I have a simple app, where I have countries which have cities, which in turn have people. I want to display list of countries in a table view. I use NSFetchedResultsController to get all the data. This is the setup :
-(void)initializeFetchedResultsController
{
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"Country"];
fetchRequest.sortDescriptors = #[[NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES]];
fetchRequest.fetchBatchSize = 30;
self.fetchResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:[NSManagedObjectContext managedObjectContext]
sectionNameKeyPath:#"nameFirstLetter"
cacheName:nil];
self.fetchResultsController.delegate = self;
[self.fetchResultsController performFetch:nil];
}
I also added an ability to search by typing in the country name in the search bar, so I implemented UISearchBarDelegate method :
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
if([searchText isEqualToString:#""])
{
self.fetchResultsController.fetchRequest.predicate = nil;
}
else
{
self.fetchResultsController.fetchRequest.predicate = [NSPredicate predicateWithFormat:#"name BEGINSWITH[cd] %#", searchText];
}
[self.fetchResultsController performFetch:nil];
[self.tableView reloadData];
}
This works but not the way I want. I expected that on predicate change, my NSFetchResultsControllerDelegate delegate methods will be called again so I can insert or delete items / sections from my table view (i want animations) without having to figure out myself, what has been added and removed. But this is not the case, instead if I change the predicate, delegate methods are not called, and I must do a simple [self.tableView reloadData]. Am I doing something wrong or is it just the way it is supposed to work, and I cannot take this shortcut ?
You are not doing it wrong. This is the way it is and there's no shortcut to take. You'll have to implement your own code to animate the table view between fetches.
As #StianHøiland says, you need to do it yourself. Generally with delegate methods they are called as a result of an 'offline' / asynchronous change in order to notify you. They are not called as a result of a change you have explicitly requested.
You could think about using the fetchedObjects and the indexPathForObject: features of the FRC. Filter the fetchedObjects list (using your predicate). Get the index paths for the objects that have been removed and you can animate them out.

Core Data: How to pass a managed object to a UITableViewCell and do a fetchRequest from there?

I have a UITableView With a custom Cell that contains another UItableView. I need to pass an object to it, and then execute a fetchrequest based on this object.
From my cellForRowAtIndexPath method, I am passing a Managed Object to the custom Cell, like this:
TWHistoryViewStandardExpendedCell *cell = (TWHistoryViewStandardExpendedCell *)[tableView dequeueReusableCellWithIdentifier:ExpandedCell];
if (cell == nil) {
cell = (TWHistoryViewStandardExpendedCell *)[[[NSBundle mainBundle] loadNibNamed:#"HistoryViewStandardCellExpanded" owner:self options:nil] objectAtIndex:0];
}
Day *aDay = (Day *)[_fetchedResultsController objectAtIndexPath:indexPath];
[cell setViewingDay: aDay]; // NSLog here returns the expected object! :)
return cell;
This way, I should be able to execute a fetchRequest based on this object, from my custom Cell.
On my custom UItableViewCell, I do this:
- (void) awakeFromNib
{
[self fetchRequest];
}
- (void)fetchRequest {
NSError *error;
if (![[self fetchedResultsController] performFetch:&error]) {
// Update to handle the error appropriately.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
exit(-1); // Fail
}
}
Some details about my fetchedResultsController: see in comments that my managed object returns null here!
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController) {
return _fetchedResultsController;
}
NSManagedObjectContext * managedObjectContext =
[myAppDelegate managedObjectContext];
NSEntityDescription *entity =
[NSEntityDescription entityForName:#"Clock"
inManagedObjectContext:managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity:entity];
[fetchRequest setPredicate: [NSPredicate predicateWithFormat:#"day == %#", _viewingDay]];
NSLog(#"_viewingDay: %#", _viewingDay); // returns null! :(
NSLog(#"_viewingDay.clocks: %#", _viewingDay.clocks); // also returns null!
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"clockIn" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
//
[fetchRequest setSortDescriptors:sortDescriptors];
[fetchRequest setFetchBatchSize:20];
NSFetchedResultsController *theFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:managedObjectContext
sectionNameKeyPath:nil
cacheName:nil];
_fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
Based on this, I have overriden my setViewingDay method:
- (void)setViewingDay:(Day *)viewingDay
{
if (_viewingDay != viewingDay) {
_viewingDay = viewingDay;
NSLog(#"setViewingDay: %#", _viewingDay); // returns expected object! =)
[self fetchRequest];
}
}
Still, after this, my UITableView remains empty. My numberOfRowsInSection method keeps returning 0!
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> sectionInfo = [[_fetchedResultsController sections] objectAtIndex:section];
NSLog(#"numberOfRowsInSection %d", [sectionInfo numberOfObjects]);
return [sectionInfo numberOfObjects];
}
I also added a UIButton that performs the fetch request. Hoping to find a situation where the fetchRequest was being performed before the actual setting of my reference to the managed object. So I click on it, do perform a fetch. And nothing! No rows whatsoever.
EDIT:
However, the NSLogs bellow, from inside my fetchResultsController do return an object then, by using a supporting UIBUtton and IBAction. But still no rows!
NSLog(#"_viewingDay: %#", _viewingDay);
NSLog(#"_viewingDay.clocks: %#", _viewingDay.clocks);
Supporting UIButton to perform a fetch:
- (IBAction)fetchem:(UIButton *)sender {
[_clocksTableView reloadData];
_fetchedResultsController = nil;
[NSFetchedResultsController deleteCacheWithName:nil];
[self fetchRequest];
NSLog(#"\n\nfetchem: %# \n\nvd: %# \n\nclocks: %# ", _fetchedResultsController.description, _viewingDay.description, _viewingDay.clocks.description );
}
Anything I might be missing here?
Thank you!
EDIT 2:
I just realized that setting my predicate to (1 == 1), returns all clocks. Confirming that my resultsController is correctly set up. There may be something wrong with my predicate... I don't see what. I have a similar predicate in a previous controller and works great.
Predicate is very simple, nothing fancy:
[fetchRequest setPredicate: [NSPredicate predicateWithFormat:#"day == %#", _viewingDay]];
I have a Clock and Day entities.
A Day has many clocks. 1 to many relationship.
A clock has one day, and one day only.
The fetchRequest above should have been returning all clocks from that day. But it's not?
1) awakeFromNib gets execute the moment you instantiate the cell and therefore before you set the viewing day. It also doesn't get execute if the cell is reused, so you should trigger the fetch request separately.
2) How does the fetchedResultsController property inside the cell get set?
EDIT 1:
From the datamodel and the predicate, it would seem the relationship between clock and day doesn't get set correctly.
EDIT 2:
Quoting the asker's comment from below: "The problem was fixed after inserting: [self fetchRequest] inside my overriden setter."

Search bar in core data project

I use core data with magical record and i'm try to filter data with a search bar in a table view.
I write two methods to get the number of rows and the name of the cells:
-(int) dammiNumeroCercati:(NSString *)searchBar
{
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"nome CONTAINS [cd] %#", searchBar];
NSArray*arra = [Ricetta MR_findAllSortedBy:#"nome" ascending:YES withPredicate:predicate];
return arra.count;
}
-(NSString*) dammiNomeRicettaCercata:(NSString *)searchBar mostrataNellaCella: (int) cella
{
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"nome CONTAINS [cd] %#", searchBar];
NSArray *arra = [Ricetta MR_findAllSortedBy:#"nome" ascending:YES withPredicate:predicate];
Ricetta*ctn = arra[cella];
return [NSString stringWithFormat:#"%#", ctn.nome];
}
then i call this method inside the numberOfRowsInSection: and cellForRowAtIndexPath: inside an if cycle:
if (self.mySearchBar.isFirstResponder){
// the above methods
} else {
// the normals methods to have all the data
}
somebody know where I'm wrong or if I miss somethings?
searchBar is usually a UISearchBar, not a string.
You should use searchBar.text and process that in your methods.
Also, in your table view's datasource methods you have to make sure which table view is causing the callback, and then return the correct count/string. Usually this is checked by comparing pointers to the two tables (original table view and search results table view).
-(NSUInteger)tableView:(UITableView*)tableView
numberOfRowsInSection:(NSUInteger)section {
if (tableView == _tableView) {
// return the usual row count
}
return [self dammiNumeroCercati:_searchBar.text];
}

Search NSArray of NSDictionary (which contains NSArray of NSDictionary, repeatedly)

I have a data-structure (in plist) that looks something like this:
What i have here is an NSArray of NSDictionary. Each NSDictionary has two keys:
Title
Link (recursive)
This forms a tree like structure, with variable length branches i.e. some branches can die at level 0, and some can be as large as level 3 or more.
I'm showing this structure in UITableView (with a little help from UINavigationController). This was easy enough.
Note: On tapping the Leaf Node
(represented by NSDictionary object
with Nil or Zero as "Link"), an
event is triggered i.e. Model window
appears with some information.
Now, i need to add Search support.
Search bar will appear above UITabeView (for Level 0). I need to come-up with a way to search this tree like structure, and then show the results using UISearchDisplayController, and then allow users to navigate the results as well.
How?... is where i'm a little stuck
and need some advise.
The search has to be quick, because we want search as you type.
p.s. I've thought of translating this data structure to CoreData, and it's still lurking in my mind. If you think it can help in this case, please advise.
Edit:
Here's my current solution, which is working (by the way):
#pragma mark -
#pragma mark UISearchDisplayController methods
- (void)searchBarResultsListButtonClicked:(UISearchBar *)searchBar {
NSLog(#"%s", __FUNCTION__);
}
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString {
NSLog(#"%s", __FUNCTION__);
[self filterCategoriesForSearchText:searchString
scope:[controller.searchBar selectedScopeButtonIndex]];
// Return YES to cause the search result table view to be reloaded.
return YES;
}
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchScope:(NSInteger)searchOption {
NSLog(#"%s", __FUNCTION__);
[self filterCategoriesForSearchText:[controller.searchBar text]
scope:[controller.searchBar selectedScopeButtonIndex]];
// Return YES to cause the search result table view to be reloaded.
return YES;
}
#pragma mark UISearchDisplayController helper methods
- (void)filterCategoriesForSearchText:(NSString *)searchText scope:(NSInteger)scope {
self.filteredCategories = [self filterCategoriesInArray:_categories forSearchText:searchText];
NSSortDescriptor *descriptor = [[[NSSortDescriptor alloc] initWithKey:KEY_DICTIONARY_TITLE ascending:YES] autorelease];
[self.filteredCategories sortUsingDescriptors:[NSArray arrayWithObjects:descriptor, nil]];
}
- (NSMutableArray *)filterCategoriesInArray:(NSArray *)array forSearchText:(NSString *)searchText {
NSMutableArray *resultArray = [NSMutableArray array];
NSArray *filteredResults = nil;
// Apply filter to array
// For some weird reason this is not working. Any guesses? [NSPredicate predicateWithFormat:#"%# CONTAINS[cd] %#", KEY_DICTIONARY_TITLE, searchText];
NSPredicate *filter = [NSPredicate predicateWithFormat:#"Title CONTAINS[cd] %#", searchText];
filteredResults = [array filteredArrayUsingPredicate:filter];
// Store the filtered results (1)
if ((filteredResults != nil) && ([filteredResults count] > 0)) {
[resultArray addObjectsFromArray:filteredResults];
}
// Loop on related records to find the matching results
for (NSDictionary *dictionayObject in array) {
NSArray *innerCategories = [dictionayObject objectForKey:KEY_DICTIONARY_LINK];
if ((innerCategories != nil) && ([innerCategories count] > 0)) {
filteredResults = [self filterCategoriesInArray:innerCategories forSearchText:searchText];
// Store the filtered results (2)
if ((filteredResults != nil) && ([filteredResults count] > 0)) {
[resultArray addObjectsFromArray:filteredResults];
}
}
}
return resultArray;
}
Core Data would be able to perform the search in the data store pretty efficiently, and would scale the search to more levels efficiently. Also, if you use NSFetchedResultsController for the TableView it would almost certainly be more memory efficient - the worst case would only have one level array loaded at any given time. And the best case is considerably better, as it would only have faulted a few objects into the array. HTH

Resources