原文:http://lovebirdegg.co.tv/2010/03/filtering-fun-with-predicates-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
%@",
@"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
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 <UIKit/UIKit.h>
@interface RootViewController : UITableViewController <UISearchDisplayDelegate, UISearchBarDelegate>
{
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.
原文:http://www.drobnik.com/touch/2010/03/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
view plain
print
?
1
%@",
@"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.