Being present longer than iPhone OS exists on the Mac platform NSPredicate was only introduced to us iPhone developers in Version 3.0 of the SDK. They have multiple interesting uses, some of which I am going to explore in this article.
You will see how you can filter an array of dictionaries, learn that the same also works for your own custom classes. Then we’ll see how to replace convoluted IF trees with simple predicates. We’ll explore how to use predicates to filter entries of a table view and finally peek into the inner workings of predicates.
Being simple and powerful at the same time it took me 3 hours to write this article. I hope you don’t give up halfway through it, because I promise it will be a great addition to your skillset as iPhone developer.
One interesting use of predicates is to filter an array for entries where a specific key matches some criteria. In the following example I am adding four people to an array in the form of individual dictionaries. Then I’m filtering for all the entries that contain the letter o in lastName.
NSMutableArray *people = [NSMutableArray array]; [people addObject:[NSDictionary dictionaryWithObjectsAndKeys: @"Oliver", @"firstName", @"Drobnik", @"lastName", nil]]; [people addObject:[NSDictionary dictionaryWithObjectsAndKeys: @"Steve", @"firstName", @"Jobs", @"lastName", nil]]; [people addObject:[NSDictionary dictionaryWithObjectsAndKeys: @"Bill", @"firstName", @"Gates", @"lastName", nil]]; [people addObject:[NSDictionary dictionaryWithObjectsAndKeys: @"Obiwan", @"firstName", @"Kenobi", @"lastName", nil]]; NSPredicate *predicate = [NSPredicate predicateWithFormat:@"lastName CONTAINS[cd] %@", @"o"]; NSArray *filteredArray = [people filteredArrayUsingPredicate:predicate]; NSLog(@"%@", filteredArray); |
Note that the [cd] next to the operator like causes it to ignore case and diacritics. Case is obvious, o = O. Diacritics are “ancillary glyphs added to a letter”, e.g. ó which is adding an accent to a plain o. With the [d] option o == ò == ö.
In the sample I am creating a new filtered array, but NSMutableArray also has a method to do it in-place. filterUsingPredicate leaves only matching items in the array.
A variety of operators is possible when dealing with string properties:
- BEGINSWITH
- CONTAINS
- ENDSWITH
- LIKE – wildcard characters ? for single characters and * for multiple characters
- MATCHES – ICU v3 style regular expression
Predicates can be very useful to avoid monstrous IF trees. You can chain multiple predicates with the logical operators AND, OR and NOT. To evaluate an expression on a specific object use the predicate’s evaluateWithObject method.
NSDictionary *person = [NSDictionary dictionaryWithObjectsAndKeys: @"Steve", @"firstName", @"Jobs", @"lastName", nil]; NSPredicate *predicate = [NSPredicate predicateWithFormat: @"firstName ENDSWITH %@ AND lastName BEGINSWITH[c] %@", @"eve", @"j"]; if ([predicate evaluateWithObject:person]) { NSLog(@"Is YES, matches"); } |
Now in the above samples we’ve only been using NSDictionary to old our firstName and lastName properties. A quick experiment shows us if this is also working for our own custom classes. Let’s create a Person class for this purpose. This only has our two properties plus an overriding description to output useful information and a class method to quickly create a Person instance.
Person.h
@interface Person : NSObject { NSString *firstName; NSString *lastName; } @property (nonatomic, retain) NSString *firstName; @property (nonatomic, retain) NSString *lastName; + (Person *)personWithFirstName:(NSString *)firstName lastName:(NSString *)lastName; @end |
Person.m
#import "Person.h" @implementation Person @synthesize firstName, lastName; + (Person *)personWithFirstName:(NSString *)firstName lastName:(NSString *)lastName { Person *person = [[[Person alloc] init] autorelease]; person.firstName = firstName; person.lastName = lastName; return person; } - (NSString *)description { return [NSString stringWithFormat:@"", NSStringFromClass([self class]), firstName, lastName]; } - (void) dealloc { [firstName release]; [lastName release]; [super dealloc]; } @end |
Now let’s see if we still get the same result if we do the same filtering of an array, this time with our own Person instances in it.
Person *person1 = [Person personWithFirstName:@"Oliver" lastName:@"Drobnik"]; Person *person2 = [Person personWithFirstName:@"Steve" lastName:@"Jobs"]; Person *person3 = [Person personWithFirstName:@"Bill" lastName:@"Gates"]; Person *person4 = [Person personWithFirstName:@"Obiwan" lastName:@"Kenobi"]; NSArray *people = [NSArray arrayWithObjects:person1, person2, person3, person4, nil]; NSPredicate *predicate = [NSPredicate predicateWithFormat:@"firstName CONTAINS[cd] %@", @"i"]; NSArray *filteredArray = [people filteredArrayUsingPredicate:predicate]; NSLog(@"%@", filteredArray); |
Yup! Still works! Now is that cool or what? One obvious use for predicates is to filter the data array in a table view controller to only match the contents of your search box.
To try this out we need to do the following:
- create a new navigation-based iPhone application, WITHOUT CoreData
- copy the Person header and implementation files to the new project
- replace the RootViewController header and implementation as shown below.
RootViewController.h
#import @interface RootViewController : UITableViewController { NSArray *people; NSArray *filteredPeople; UISearchDisplayController *searchDisplayController; } @property (nonatomic, retain) NSArray *people; @property (nonatomic, retain) NSArray *filteredPeople; @property (nonatomic, retain) UISearchDisplayController *searchDisplayController; @end |
RootViewController.m
#import "RootViewController.h" #import "Person.h" @implementation RootViewController @synthesize people, filteredPeople; @synthesize searchDisplayController; - (void)dealloc { [searchDisplayController release]; [people release]; [filteredPeople release]; [super dealloc]; } - (void)viewDidLoad { [super viewDidLoad]; self.title = @"Search People"; Person *person1 = [Person personWithFirstName:@"Oliver" lastName:@"Drobnik"]; Person *person2 = [Person personWithFirstName:@"Steve" lastName:@"Jobs"]; Person *person3 = [Person personWithFirstName:@"Bill" lastName:@"Gates"]; Person *person4 = [Person personWithFirstName:@"Obiwan" lastName:@"Kenobi"]; people = [[NSArray alloc] initWithObjects:person1, person2, person3, person4, nil]; // programmatically set up search bar UISearchBar *mySearchBar = [[UISearchBar alloc] init]; [mySearchBar setScopeButtonTitles:[NSArray arrayWithObjects:@"First",@"Last",nil]]; mySearchBar.delegate = self; [mySearchBar setAutocapitalizationType:UITextAutocapitalizationTypeNone]; [mySearchBar sizeToFit]; self.tableView.tableHeaderView = mySearchBar; // programmatically set up search display controller searchDisplayController = [[UISearchDisplayController alloc] initWithSearchBar:mySearchBar contentsController:self]; [self setSearchDisplayController:searchDisplayController]; [searchDisplayController setDelegate:self]; [searchDisplayController setSearchResultsDataSource:self]; [mySearchBar release]; } #pragma mark Table view methods - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { if (tableView == self.searchDisplayController.searchResultsTableView) { return [self.filteredPeople count]; } else { return [self.people count]; } } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; } Person *person; if (tableView == self.searchDisplayController.searchResultsTableView) { person = [self.filteredPeople objectAtIndex:indexPath.row]; } else { person = [self.people objectAtIndex:indexPath.row]; } cell.textLabel.text = [NSString stringWithFormat:@"%@ %@", person.firstName, person.lastName]; return cell; } #pragma mark Content Filtering - (void)filterContentForSearchText:(NSString *)searchText scope:(NSString *)scope { NSPredicate *predicate; if ([scope isEqualToString:@"First"]) { predicate = [NSPredicate predicateWithFormat: @"firstName CONTAINS[cd] %@", searchText]; } else { predicate = [NSPredicate predicateWithFormat: @"lastName CONTAINS[cd] %@", searchText]; } self.filteredPeople = [people filteredArrayUsingPredicate:predicate]; } #pragma mark UISearchDisplayController Delegate Methods - (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString { [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 { [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; } @end |
Adding the search display controller is responsible for most of the additional code in this example. Just getting the filtered people to match our search has become very simple due to NSPredicate as you can see in the filterContentForSearchText method. The two UISearchDisplayController Delegate methods are called whenever you type something in the search box or switch between the scope buttons. In this case I am showing how to switch between searching in first names and last names.
The table view for the search results is actually dynamically created when needed. As it’s using the same data source and delegate methods as the original table view we need to respond differently based on which table view the methods are being called for. This is the reason for the IF in each of these methods. If we are in the search results we take the filteredPeople array, otherwise we use the original people array.
NSPredicate was only introduced into the iPhone SDKs as of version 3.0, so my guess is that there might a few instances in your code where you could simplify the logic with replacing a big IF tree with a simple predicate. Down the road, they are the only method how you can filter data coming from a fetch in CoreData.
In this article I’ve only used the predicteWithFormat method to create them. That’s actually a tremendous shortcut, because internally predicates are themselves consisting of several parts, mostly NSExpression instances. So if you feel that your code has become way too easy to understand by using predicates you can also replace them with the original composition.
Using expressions the general approach is to define a left hand expression and a right hand expression and put these into an NSComparisonPredicate. I’m just showing this here so that you can appreciate the simplicity of the shortcut method presented earlier.
NSExpression *lhs = [NSExpression expressionForKeyPath:@"firstName"]; NSExpression *rhs = [NSExpression expressionForConstantValue:@"i"]; NSPredicate *predicate = [NSComparisonPredicate predicateWithLeftExpression:lhs rightExpression:rhs modifier:NSDirectPredicateModifier type:NSContainsPredicateOperatorType options:NSCaseInsensitivePredicateOption | NSDiacriticInsensitivePredicateOption]; // same as: //NSPredicate *predicate = [NSPredicate predicateWithFormat:@"firstName CONTAINS[cd] %@", @"i"]; |
Component predicates are achieved the long way in a similar fashion by using NSCompoundPredicate, but there is no using going into these dark depths when the shortcut is so much more convenient.
Finally another hint without going into details: Predicate Templates. You can define any predicate with $Variables instead of an expression. Then when you need them you can use [template predicateWithSubstitutionVariables:] with a dictionary of values to substitute for the $Variables to prep a predicate ready for use.
Categories: Recipes
Just wanted to say thanks for taking the time to post this example. Was very helpful to me in trying to learn Core Data for iOS.
Dave
Like that last guy, just wanted to post a thanks! Ive still new to core data and got on google wondering if it was possible to use multiple predicates. This was the first site my search pull sand was exactly what i was looking for!
Thank you for the great tutorial, i really appreciate it..
How would i check the length of a string in a predicate? is this possible i want to filter the results to contain only “Event”s who’s ([description length] > 0 )
Please help if you can
Thanks, good one …..
Just compare it to NOT “”
[self setSearchDisplayController:searchDisplayController];
It is a private API, I use this code and my app was rejected!!!
hey.. this is nice and concise… i’m curious though… can NSPredicates be used to replace objects in an array with values from a dictionary?
//so for example if i had an NSDictionary like this:
NSMutableDictionary *valuesDictionary = [NSMutableDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithDouble:-60.0],@”a”,
[NSNumber numberWithDouble:0.0],@”b”,
[NSNumber numberWithDouble:-12.0],@”c”,
[NSNumber numberWithDouble:.3],@”x”, nil];
//and an array like this:
NSArray *program = [NSArray arrayWithObjects: @”a”,@”12.6″,@”100″,@”x”,nil];
//is there a nice way with NSPredicate to make a new array that would end up like this?
//@”-60″,@”12.6″,@”100″,@”.3″,nil
//replacing any variables found in the array with their valueforkey’s?
-Dave
Not by itself, because a predicate evaluates to true or false.
ok heh, let me rephrase that…
what would the code look like to result in an array programWithVariableValues, for example, consisting of @”-60″,@”12.6″,@”100″,@”.3″,nil?
helloooooo? anybody have a good answer for me?
Really saved me to write a long big code to filter out the array.
very helpful tutorial.