Building a search with some custom objects and three scopes: All, Active, and Former. Got it working with the below code:
- (void)filterContentForSearchText:(NSString*)searchText scope:(NSString *)scope {
[[self filteredArtists] removeAllObjects];
for (HPArtist *artist in [self artistList]) {
if ([scope isEqualToString:#"All"] || [[artist status] isEqualToString:scope]) {
NSComparisonResult result = [[artist displayName] compare:searchText options:(NSCaseInsensitiveSearch|NSDiacriticInsensitiveSearch) range:NSMakeRange(0, [searchText length])];
if (result == NSOrderedSame) {
[[self filteredArtists] addObject:artist];
}
}
}
}
This works fine and takes scope into account. Since I wanted to search four fields at at time, this question helped me come up with the below code:
- (void)filterContentForSearchText:(NSString*)searchText scope:(NSString *)scope {
[[self filteredArtists] removeAllObjects];
NSPredicate *resultPredicate = [NSPredicate predicateWithFormat:#"familyName CONTAINS[cd] %# OR familyKanji CONTAINS[cd] %# OR givenName CONTAINS[cd] %# OR givenKanji CONTAINS[cd] %#", searchText, searchText, searchText, searchText];
[[self filteredArtists] addObjectsFromArray:[[self artistList] filteredArrayUsingPredicate:resultPredicate]];
}
However it no longer takes scope into account.
I have been playing around with if statements, adding AND scope == 'Active', etc. to the end of the statement and using NSCompoundPredicates to no avail. Whenever I activate a scope, I'm not getting any matches for it.
Just a note that I've seen approaches like this one that take scope into account, however they only search inside one property.
Here's about how I would do it:
NSPredicate * template = [NSPredicate predicateWithFormat:#"familyName CONTAINS[cd] $SEARCH "
#"OR familyKanji CONTAINS[cd] $SEARCH "
#"OR givenName CONTAINS[cd] $SEARCH "
#"OR givenKanji CONTAINS[cd] $SEARCH"];
- (void)filterContentForSearchText:(NSString *)search scope:(NSString *)scope {
NSPredicate * actual = [template predicateWithSubstitutionVariables:[NSDictionary dictionaryWithObject:search forKey:#"SEARCH"]];
if ([scope isEqual:#"All"] == NO) {
NSPredicate * scopePredicate = [NSPredicate predicateWithFormat:#"scope == %#", scope];
actual = [NSCompoundPredicate andPredicateWithSubpredicates:[NSArray arrayWithObjects:actual, scopePredicate, nil]];
}
[[self filteredArtists] setArray:[[self artistList] filteredArrayUsingPredicate:actual]];
}
Related
I'm using to get some of the content of nsmutable array and it work fine if I don't use nsstring to make the query:
NSLog(#"user information %#", [usersInfo filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"%K == 'Joe", #"id"]]);
But try to use a nsstring to query for the user it doesn't work:
NSString *user="Joe";
NSLog(#"user information %#", [usersInfo filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"%K == user", #"id"]]]);
any of you knows what I'm doing wrong? or what would be the best of doing it using NSString to query for users?
When you write
NSString *user = #"Joe";
... [NSPredicate predicateWithFormat:#"%K == user", #"id"]
you seem to expect that "user" in the predicate is replaced by the contents ("Joe") of the NSString variable, but this is not correct.
You have to give the string
as another argument to the predicate and add the %# format that will be expanded by the string.
NSString *user = #"Joe";
... [NSPredicate predicateWithFormat:#"%K == %#", #"id", user]
Here %K (which is var arg substitution for a key path) will be
substituted by the key "id", and %# (which is
var arg substitution for an object value) will be substituted
by the contents of the user variable.
Using %K expansion instead of
[NSPredicate predicateWithFormat:#"id == %#", user]
has the advantage that it works correctly even if the key is a
reserved word in the predicate format string syntax.
I am unable to gues why you are using "%K == 'Joe"
Anyways, You can use predicate as:
[NSPredicate predicateWithFormat:#"object.property LIKE[c] %#", stringValue];
Or,
[NSPredicate predicateWithFormat:#"object.property==[c] %#", stringValue];
try this:
NSString *user="Joe";
NSLog(#"user information %#", [usersInfo filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"SELF contains [cd] %#", user]]]);
Make your predicate format string before don't try to add property name inside predicate format :
NSString *predicateFormat = [NSString stringWithFormat:#"%# == %%#",#"id"];
NSPredicate *predicate = [NSPredicate predicateWithFormat: predicateFormat, #"Joe"];
NSArray *filteredArray = [mutableArray filteredArrayUsingPredicate:predicate];
Use:
NSLog(#"user information %#", [usersInfo filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:[NSString stringWithFormat:#"%K == '%#'",#"id",user]]]);
When you use:
NSLog(#"user information %#", [usersInfo filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"%K == user", #"id"]]]);"
The user will be a part of the string, it won't replace with the content of the NSString object.
I have a Person Object which has two NSString properties; firstName and lastName.
I'm currently using an NSPredicate like so:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"(firstName contains[cd] %#) OR (lastName contains[cd] %#)", searchText, searchText];
So, for example, say I'm searching for the name "John Smith". In my search bar if I type "Joh", then John Smith will appear as an option. This is good and fine, but if I type in "John Sm" it will go blank.
How can I join firstName and lastName in the predicate so if I was searching for "John Sm" then John Smith would still appear as an option.
I hope this makes sense. Thanks.
EDIT:
To add a bit more clarification, I'm using the SearchDisplayController delegate method:
-(void)filterContentForSearchText:(NSString *)searchText scope:(NSString *)scope;
and I'm using the predicate like so:
newArray = [personObjectArray filteredArrayUsingPredicate:predicate];
Try this,
NSString *text = #"John Smi";
NSString *searchText = [text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
NSArray *array = [searchText componentsSeparatedByString:#" "];
NSString *firstName = searchText;
NSString *lastName = searchText;
NSPredicate *predicate = nil;
if ([array count] > 1) {
firstName = array[0];
lastName = array[1];
predicate = [NSPredicate predicateWithFormat:#"(firstName CONTAINS[cd] %# AND lastName CONTAINS[cd] %#) OR (firstName CONTAINS[cd] %# AND lastName CONTAINS[cd] %#)", firstName, lastName, lastName, firstName];
} else {
predicate = [NSPredicate predicateWithFormat:#"firstName CONTAINS[cd] %# OR lastName CONTAINS[cd] %#", firstName, lastName];
}
NSArray *filteredArray = [people filteredArrayUsingPredicate:predicate];
NSLog(#"%#", filteredArray);
Output:
(
{
firstName = John;
lastName = Smith;
}
)
Here text represents the searched text. The advantage with the above is, even if you pass text = #"Smi Joh"; or text = #"John "; or text = #" smi"; or text = #"joh smi ";, it will still show the above output.
The solution suggested above will not work with search strings that have more than two words. Here is a more thorough implementation in swift. This solution also allows for adding more fields on a record, if your goal is to implement a full text search across name, email, phone number, etc. In that case just update the NSPredicate to OR newField CONTAINS[cd] %# and be sure to add the extra $0 in the list of string replacements.
let searchText = search.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet())
let words = searchText.componentsSeparatedByCharactersInSet(NSCharacterSet.whitespaceCharacterSet())
let predicates = words.map { NSPredicate(format: "firstName CONTAINS[cd] %# OR lastName CONTAINS[cd] %#", $0,$0) }
let request = NSFetchRequest()
request.predicate = NSCompoundPredicate(type: NSCompoundPredicateType.AndPredicateType, subpredicates: predicates)
you can concatenate the fields into two common fields (firstLastName and lastFirstName )
- (NSString *)firstLastName {
return [NSString stringWithFormat:#"%# %#", self.firstName, self.lastName];
}
- (NSString *)lastFirstName {
return [NSString stringWithFormat:#"%# %#", self.lastName, self.firstName];
}
and then filter on this fields using 'contains[cd]'
[NSPredicate predicateWithFormat:#"(firstLastName contains[cd] %#) OR (lastFirstName contains[cd] %#)" , self.searchBar.text, self.searchBar.text];
In my application i have a large table of around 12000 entries. I am displaying it on tableview. But the search bar is too slow while doing dynamic search. I have read that NSPredicate method is more permorfant then NSRange.
This is my old the code:
[self.filteredListContent removeAllObjects];
listContent = [[NSArray alloc] initWithArray:[dbAccess getAllBooks]];
for (Book *book in listContent)
{
NSRange range = [book.textBook rangeOfString:searchText options:NSCaseInsensitiveSearch];
if (range.location != NSNotFound)
{
[self.filteredListContent addObject:book];
}
}
My new code:
[self.filteredListContent removeAllObjects];
listContent = [[NSArray alloc] initWithArray:[dbAccess getAllBooks]];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"SELF like[c] %#",searchText];
[self.filteredListContent addObject:[listContent filteredArrayUsingPredicate:predicate]];
When i try to execute this code i received this error: "Can't do regex matching on object .'"
I would do something more like...
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"%k like[c] %#",propertyIAmLookingFor,searchText];
Is your book class a string? If not then you cant use SELF like. You need to substitute the name of the property you are comparing.
I have a UISearchResultsController which is filtering my NSFetchedResultsController and putting the filtered data into an array. Currently, I use an NSPredicate to take the search bar content and apply the filter. Here's my predicate:
[filteredArray removeAllObjets];
for(Account *account in unfilteredResults){
NSPredicate *predicate;
if(controller.searchBar.selectedScopeButtonIndex == 0){
predicate = [NSPredicate predicateWithFormat:#"accountFirstName BEGINSWITH[cd] %#", searchString];
}else if(controller.searchBar.selectedScopeButtonIndex == 1){
predicate = [NSPredicate predicateWithFormat:#"accountLastName BEGINSWITH[cd] %#", searchString];
}else if(controller.searchBar.selectedScopeButtonIndex == 2){
predicate = [NSPredicate predicateWithFormat:#"group.groupName CONTAINS[cd] %#", searchString];
}
if([predicate evaluateWithObject:account]){
[self.filteredArray addObject:account];
}
}
I'm trying to filter the accounts based on either first name, last name, or group name. I know that these operations are slow, but what can be done to make them faster?
Edit:
I just noticed that I'm recreating the predicate every iteration, but I still think that there should be a better way. I did see something about doing a binary compare in an Apple video, but I have no idea how to convert a search string into a binary string and less how to get the "next greatest" value.
How do I replace the BEGINSWITH statement with something more efficient?
I'm assuming your data objects are CoreData objects, if so have you marked the properties you are searching on as indexed?
It is too old post, but can help to somebody else. With this code you will create only one instance of NSPredicate.
NSPredicate *predicate;
if(controller,searchBar,.selectedScopeButtonIndex == 0){
predicate = [NSPredicate predicateWithFormat:#"accountFirstName BEGINSWITH[cd] %#", searchString];
}else if(controller,searchBar,.selectedScopeButtonIndex == 1){
predicate = [NSPredicate predicateWithFormat:#"accountLastName BEGINSWITH[cd] %#", searchString];
}else if(controller,searchBar,.selectedScopeButtonIndex == 2){
predicate = [NSPredicate predicateWithFormat:#"group.groupName CONTAINS[cd] %#", searchString];
}
self.filteredArray = [unfilteredResults filteredArrayUsingPredicate:predicate];
In one of the code examples from Apple, they give an example of searching:
for (Person *person in personsOfInterest)
{
NSComparisonResult nameResult = [person.name compare:searchText
options:(NSCaseInsensitiveSearch|NSDiacriticInsensitiveSearch)
range:NSMakeRange(0, [searchText length])];
if (nameResult == NSOrderedSame)
{
[self.filteredListContent addObject:person];
}
}
Unfortunately, this search will only match the text at the start. If you search for "John", it will match "John Smith" and "Johnny Rotten" but not "Peach John" or "The John".
Is there any way to change it so it finds the search text anywhere in the name? Thanks.
Try using rangeOfString:options: instead:
for (Person *person in personsOfInterest) {
NSRange r = [person.name rangeOfString:searchText options:(NSCaseInsensitiveSearch|NSDiacriticInsensitiveSearch)];
if (r.location != NSNotFound)
{
[self.filteredListContent addObject:person];
}
}
Another way you could accomplish this is by using an NSPredicate:
NSPredicate *namePredicate = [NSPredicate predicateWithFormat:#"name CONTAINS[cd] %#", searchText];
//the c and d options are for case and diacritic insensitivity
//now you have to do some dancing, because it looks like self.filteredListContent is an NSMutableArray:
self.filteredListContent = [[[personsOfInterest filteredArrayUsingPredicate:namePredicate] mutableCopy] autorelease];
//OR YOU CAN DO THIS:
[self.filteredListContent addObjectsFromArray:[personsOfInterest filteredArrayUsingPredicate:namePredicate]];
-[NSString rangeOfString:options:] and friends are what you want. It returns:
"An NSRange structure giving the location and length in the receiver of the first occurrence of aString, modulo the options in mask. Returns {NSNotFound, 0} if aString is not found or is empty (#"")."