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
Related
I have a plist (an array of dictionary's) that I am reading into an NSArray which I am using to populate a table. It's a list of people and their work location, phone number, etc. I added a UISearchBar and implemented the textDidChange method as well.
When I search by the person's last name I do see the filtered list in the table, however I don't think that I am storing the filtered results properly. I am using an NSMutable Array but I am losing the key:value pairing.
Can someone please point me in the right direction? I ultimately would like to click on a filtered name and push to a detailed view controller. I believe my issue is that I am trying to capture the filtered results in an NSMutableArray but I am not certain.
I've done a lot of Googling but can't seem to put this together in my head. Any help is appreciated.
Kind Regards,
Darin
Here is the array that I am using to load the plist.
-(NSArray *)content
{
if (!content){ {
content = [[NSArray alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"ophonebook" ofType:#"plist"]];
NSSortDescriptor* nameSortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"Last" ascending:YES];
content= [content sortedArrayUsingDescriptors:[NSArray arrayWithObject:nameSortDescriptor]];
}
return content;
}
Here is the UISearchBar Method
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {
if (searchText.length == 0)
{
isFiltered= NO;
} else {
isFiltered= YES;
filteredPeople = [[NSMutableArray alloc]init];
for (NSDictionary *dict in content)
{
NSString *last = [dict objectForKey:#"Last"];
NSRange lastRange = [last rangeOfString:searchText options:NSCaseInsensitiveSearch];
if (lastRange.location != NSNotFound)
{
[filteredPeople addObject: [dict objectForKey:#"Last"]];
}
}
}
[myTableView reloadData];
}
Keep things simple, get rid of the flags and the different arrays in your table delegate methods. Instead, have 1 array for your source data (content) and another array for your source data that is actually for display (call it displayContent).
Now, when you start up, set displayContent = content.
Then, when you want to filter, set displayContent = [content filteredArrayUsingPredicate:...]; (you can convert your current loop into a simple predicate).
Finally, when you're done searching, set displayContent = content.
No flags. No ifs in the table delegate methods. Simple, readable code.
p.s. your problem is:
[filteredPeople addObject: [dict objectForKey:#"Last"]];
which you should be setting to:
[filteredPeople addObject:dict];
so you have all the data instead of just the names. Though technically you could still make it work by searching for the last name in your content.
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];
}
I have an iPad app (Xcode 4.6, iOS 6.2, ARC and Storyboards). I have a UITableView that contains prototype cells, with two labels (lName and lPhone). I have filled a NSArray with the results of the Core Data store. I copied the code from a sample, and am lost! I have two fields I am looking for: name and phone number. I want to be able to search on either one. I tried using the UISearchBar Controller, but the results span the entire window, which is not acceptable. So, I'm trying to do this without the controller. I want the search to filter the shown entries in the UITableView, which this bit of code is supposed to do.
When I do the MR_findAll (MagicalRecord), I get all of the attributes in the Core Data store. This is where I'm lost - how do I get the two attributes out of the array and into the NSMutableArray allTableData, or is it even necessary in this case?
This is my code, so far:
NSArray *allDataArray = [ClientInfo MR_findAll];
// move objects from Core Data store to NSMutablearray
[allTableData addObjectsFromArray:allDataArray];
if(text.length == 0) {
isFiltered = FALSE;
}
else {
isFiltered = true;
filteredTableData = [[NSMutableArray alloc] init];
for (ClientCell* client in allTableData) {
NSRange nameRange = [client.lName.text rangeOfString:text options:NSCaseInsensitiveSearch];
NSRange phoneRange = [client.lPhone.text rangeOfString:text options:NSCaseInsensitiveSearch];
if(nameRange.location != NSNotFound || phoneRange.location != NSNotFound) {
[filteredTableData addObject:client];
}
}
}
I also don't understand how the NSRange is going to match against the two fields I'm looking for. I'm really confused here.
The rangeOfString method returns an NSRange with a location that's not equal to NSNotFound when a given substring is found in the receiver string. What your code does is that it first checks the range of the search text in client.lName.text and client.lPhone.text. Next, the code adds the object to filteredTableData if either of the ranges exist.
As for adding only your two attributes to the filteredTableData, this is simply not needed, as you should access the already stored object to fetch these attributes.
Finally, I'd also recommend you have a look at the free Sensible TableView framework as it should help you perform these kind of searches automatically.
You have to Have to do some thing like this
Fetch the Data from coredata into an array which is pretty mandatory and you have lot of tutorials on that.
And then in the search bar delegate method do implement some thing like this. Which will start filtering your array so that you can see your desired results
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
self.tableView.allowsSelection = YES;
self.tableView.scrollEnabled = YES;
NSArray *list = [[NSArray alloc] initWithArray:artists];
if (searchBar.text && [searchBar.text length] > 0)
{
NSMutableArray *filterContacts = [[NSMutableArray alloc]initWithArray:list];
// NSPredicate *predicate = [NSPredicate predicateWithFormat:#"title LIKE %#",searchBar.text ];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"title CONTAINS[c] %#",searchBar.text ];
[filterContacts filterUsingPredicate:predicate];
artists = filterContacts;
}
[self.tableView reloadData];
[self updateSearchString:searchBar.text];
}
I've checked StackOverflow for some of the topics on NSPredicates, and although they all point in the right direction, I must be missing something essential about it all.
I've got an NSMutableArray that contains a list of products.
Each product has several properties like Brand, Category and Type.
Now I want my users to be able to filter that NSMutableArray using NSPredicates, insomuch that if any of the selected filters are blank, it shouldn't use that filter.
But, in turn, if for example all filters are on: Filter with Brand A with Category B and Type C, it should only show Brand A with Cat B and Type C.
Should I then deselect Cat B, it would filter on Brand A with Type C.
I've written some code, but it mainly returns an empty NSMutableArray, so I guess my NSPredicates are off.
I also found out that I need to default to the 'all products' NSMutableArray before running the predicate, or it will filter the already filtered array when a new filter option is selected. Should I use multiple Arrays with some BOOLean magick, or is this an issue that can be solved using NSPredicates?
Here's my code:
-(void)filterTable
{
NSPredicate *brandPredicate;
NSPredicate *categoryPredicate;
NSMutableArray *compoundPredicateArray;
if( ![self.selectedBrand isEqual: #"Show All Brands"] || !(self.currentBrand == NULL))
{
brandPredicate = [NSPredicate predicateWithFormat:#"brand CONTAINS[cd] %#",self.currentBrand];
compoundPredicateArray = [ NSMutableArray arrayWithObject: brandPredicate ];
}
if( ![self.currentCategory isEqual: #"Show All Categories"] || !(self.currentCategory == NULL))
{
categoryPredicate = [NSPredicate predicateWithFormat:#"category CONTAINS[cd] %#",self.currentCategory];
[ compoundPredicateArray addObject: categoryPredicate];
}
NSPredicate *predicate = [NSCompoundPredicate andPredicateWithSubpredicates:
compoundPredicateArray ];
[self.tableData filterUsingPredicate:predicate];
[self.popNTwinBee dismissPopoverAnimated:YES]; // PopoverController
[self.tableView reloadData];
}
You have a couple of conceptual errors in your code.
First, you should init your NSMutableArray of predicates as you declare it:
NSMutableArray *compoundPredicateArray = [NSMutableArray array];
Right now you only instantiate it inside your first if(), so that if the brand filter is not set the mutable array doesn't even get instantiated so adding objects to it later (for example in the second filtering if()) is uneffective and the compound predicate created empty.
Inside your first if() you will then have:
[compoundPredicateArray addObject:brandPredicate];
Your second issue is that, as you correctly imagined, you are filtering what you have already filtered previously, when you use filterUsingPredicate.
What you should be doing is to always keep the unfiltered data in a NSArray and use the filteredArrayUsingPredicate method on it to retrieve a new filtered NSArray you will use to display the data from.
Well I took a good look at the code and came up with this:
Got this handy little block of code from this site.
NSArray+filter.h
#import <Foundation/Foundation.h>
#interface NSArray (Filter)
- (NSArray*)filter:(BOOL(^)(id elt))filterBlock;
#end
NSArray+filter.m
#import "NSArray+filter.h"
#implementation NSArray(Filter)
- (NSArray*)filter:(BOOL(^)(id elt))filterBlock
{ // Create a new array
id filteredArray = [NSMutableArray array]; // Collect elements matching the block condition
for (id elt in self)
if (filterBlock(elt))
[filteredArray addObject:elt];
return filteredArray;
}
#end
And edited my method accordingly.
In TableViewController.m
- (void)filterTable {
WebServiceStore *wss = [WebServiceStore sharedWebServiceStore];
self.allProducts = [wss.allProductArray mutableCopy];
NSArray *filteredOnBrand;
NSArray *filteredOnCategory;
NSArray *filteredOnCategoryAndBrand;
if (![self.currentBrand isEqualToString:#"All Brands"] && !(self.currentBrand == nil))
{
filteredOnBrand = [self.allProducts filter:^(id elt)
{
return [[elt brand] isEqualToString:self.currentBrand];
}];
[self.tableData removeAllObjects];
[self.tableData addObjectsFromArray:filteredOnBrand];
}
if ([self.currentBrand isEqualToString:#"All Brands"] || self.currentBrand == nil)
{
filteredOnBrand = [self.allProducts mutableCopy];
[self.tableData removeAllObjects];
[self.tableData addObjectsFromArray:filteredOnBrand];
}
if (![self.currentCategory isEqualToString:#"All Categories"] && !(self.currentCategory == nil))
{
filteredOnCategory = [self.allProducts filter:^(id elt)
{
return [[elt category] isEqualToString:self.currentCategory];
}];
[self.tableData removeAllObjects];
[self.tableData addObjectsFromArray:filteredOnCategory];
}
if (![self.currentCategory isEqualToString:#"All Categories"] && !(self.currentCategory == nil) && ![self.currentBrand isEqualToString:#"All Brands"] && !(self.currentBrand == nil)) {
filteredOnBrand = [self.allProducts filter:^(id elt) {
return [[elt brand] isEqualToString:self.currentBrand];
}];
filteredOnCategoryAndBrand = [filteredOnBrand filter:^(id elt) {
return [[elt category] isEqualToString:self.currentCategory];
}];
[self.tableData removeAllObjects];
[self.tableData addObjectsFromArray:filteredOnCategoryAndBrand];
}
}
You should also reload the table data afterwards of course, but I used a custom method for that, which I left out.
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.