BuySellAds.com

My book Barcodes with iOS 7 is nearing completion. Buy it now to get early access!
Our DNA is written in Objective-C
Jump

Filtering Fun with Predicates

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:

  1. create a new navigation-based iPhone application, WITHOUT CoreData
  2. copy the Person header and implementation files to the new project
  3. 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

%d bloggers like this: