Searching arrays with NSPredicate and blocks

In researching a change I wanted to make to the example RSS Reader app that I have working on for this blog I started looking at the best ways to search and filter arrays. I have previously posted on filtering arrays with NSPredicate so I will only briefly revisit that topic. More interesting is using some of the newer block based methods introduced with iOS 4.0 to efficiently search a collection of objects.

Filtering Arrays with NSPredicate.

The NSArray class has the method filteredArrayUsingPredicate: which will return a new NSArray containing the objects for which a predicate returns true. So for example if I have an array of NSString I can get a new array containing only the strings starting with the letter ‘a’ (case insensitive) as follows:

NSArray *names = [[NSArray alloc]
                   initWithObjects:@"Alpha",
                                   @"Bravo", 
                                   @"Charlie",
                                   nil];
NSPredicate *predicate = [NSPredicate predicateWithFormat:
                          @"SELF beginswith[c] 'a'"];
NSArray *aNames = [names filteredArrayUsingPredicate:predicate];

In situations where we have a collection of more complex objects, for example a Person class with an ivar named lastName we can also create a predicate that queries the ivar. So to create a new array containing all people whose last name begins with ‘a’:

NSArray *people = [[NSArray alloc] initWithObjects:me,
                                   you, nil];
NSPredicate *preda = [NSPredicate predicateWithFormat:
                      @"lastName beginswith[c] 'a'"];
NSArray *aList = [people filteredArrayUsingPredicate:preda];

In fact you can follow any key-path in a predicate. So if my Person class contains a reference to an Employer object a query to find all Person objects working for “Apple” would like this:

NSPredicate *applePred = [NSPredicate predicateWithFormat:
                          @"employer.name == 'Apple'"];
NSArray *appleEmployees = [people filteredArrayUsingPredicate:applePred];

The NSMutableArray class has an additional method filterUsingPredicate: that is used the same way but with the important difference that the array is filtered in place. So instead of getting back a new NSArray the NSMutableArray is modified so that it only contains objects that match the predicate. If you do not want to modify the original NSMutableArray you can of course use filteredArrayUsingPredicate: to get back a new NSArray.

Searching an Array Using Blocks

So far this is nothing new, but there are many situations where you just need to know if something already exists in a collection and you do not need to get back the matching objects. In this situation using filteredArrayUsingPredicate returns an array that we do not need.

One option is to go back to iterating over the array applying a test for the condition we are interested in (I had such an example in my last post):

NSString *query = @"Alpha";
BOOL found = NO;
for (Person *person in people) {		
  if ([person.lastName isEqualToString:query]) {
    found = YES;
  }
}

This looks ideal for replacing with a predicate - but one where we do not get back an array:

- (BOOL)personExists:(NSString *)key
           withValue:(NSString *)value {
  
  NSPredicate *exists = [NSPredicate predicateWithFormat:
                         @"%K MATCHES[c] %@", key, value];
  NSUInteger index = [self.people indexOfObjectPassingTest:
             ^(id obj, NSUInteger idx, BOOL *stop) {
               return [exists evaluateWithObject:obj];
             }];

  if (index == NSNotFound) {
    return NO;
  }
  
  return YES;
}

In this case I am using a predicate with a dynamic property name that is substituted for the %K parameter in the format string. This is perfect for situations where you want to change the property name you are searching for at run-time.

The new step is the use of the indexOfObjectPassingTest: method which takes a block as its argument and returns an index to the first object that matches the predicate or NSNotFound if no object matches. The syntax for the block is a little hard to understand. The general structure of the method is as follows:

[myArray indexOfObjectPassingTest:
  ^(id obj, NSUInteger idx, BOOL *stop) {
    return [obj someTestForValue: value];
}];

The start of the block is identified by the “^” character and what follows are the arguments that will be passed to the block. Each object in the array is then processed in turn by calling the contents of the block with the block arguments. So for example, the obj variable can be used to access the current object from the array, idx gives the index into the array of the array and finally the stop boolean allows you to abort the processing of the array from within the block.

We can put any test we like into the content of the block as long as it returns a BOOL. In our case we can use a predicate and pass it the current object for evaluation using the evauateWithObject method. I leave it for you to decide if the approach using Predicates and Blocks is easy to understand compared to the simpler approach of iterating through the array. I have to say that I still find the syntax for Objective-C blocks to be especially ugly but since more and more of the Cocoa frameworks are adopting blocks it is worth starting to get the hang of them.