Cancellable asynchronous searching with UISearchDisplayController

Apple’s UISearchDisplayController is a handy wrapper around searching for UITableViewControllers. UISearchDisplayController will send its delegate the following message whenever a user types something into the search bar:

 - (BOOL)searchDisplayController:(UISearchDisplayController *)controller 
shouldReloadTableForSearchString:(NSString *)searchString

It is in this method that the delegate should perform a search of its data and based on whether the search results have or would change return the value YES or NO.

This all works fine and dandy when the objects to search are in memory and the delegate method executs promptly. If this is not the case then the UI will block while the search logic is performed. This can create significant lag in the user interface as the user is typing into the search field.

In the iOS API documentation for searchDisplayController:shouldReloadTableForSearchString: Apple provides a hint about performing search work in the background but doesn’t really go into any detail. The docs say:

You might implement this method if you want to perform an asynchronous search. You would initiate the search in this method, then return NO. You would reload the table when you have results.

An approach to doing an asynchronous search might be something like this:

@interface MyUISearchDisplayDelegate ()
@property (nonatomic, retain) NSOperationQueue *searchQueue;
@property (nonatomic, retain) NSArray *searchResults;
@end

@implementation MyUISearchDisplayDelegate
@synthesize searchQueue, searchResults;

- (id)init
{
    if ((self = [super init]))
    {
        self.searchResults = [NSMutableArray array];
        self.searchQueue = [[NSOperationQueue new] autorelease];
        [self.searchQueue setMaxConcurrentOperationCount:1];
    }
    
    return self;
}

- (void)dealloc
{
    self.searchResults = nil;
    self.searchQueue = nil;
    
    [super dealloc];
}

// ...

 - (BOOL)searchDisplayController:(UISearchDisplayController *)controller
shouldReloadTableForSearchString:(NSString *)searchString
{
   if  ([searchString isEqualToString:@""] == NO)
    {
        [self.searchQueue addOperationWithBlock:^{
            
            NSArray *results = // fetch the results from 
                // somewhere (that can take a while to do)
            
            // Ensure you're assigning to a local variable here.
            // Do not assign to a member variable.  You will get
            // occasional thread race condition related crashes 
            // if you do.            

            [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                // Modify your instance variables here on the main
                // UI thread.
                [self.searchResults removeAllObjects]; 
                [self.searchResults addObjectsFromArray:results];
                
                // Reload your search results table data.
                [controller.searchResultsTableView reloadData];
            }];
        }];
        
        return NO;
    }
    else
    {
        [self.searchResults removeAllObjects];
        return YES;
    }
}

This implementation gets you some of the way there. The problem with it however is that as the user interacts with the UISearchBar the searchString changes and with each character typed another invocation of searchDisplayController:shouldReloadTableForSearchString: is performed. This causes lots of work to be enqueued into the NSOperationQueue. Work that must get executed in sequence.

This is not ideal. The user is only interested in the most recent search string that they have entered. If they enter the string “fubar” into the UISearchBar they don’t care about the searches for f, fu, fub, and fuba.

OK. So how do you properly implement this? Well, it is surprisingly easy. Just cancel all the existing operations in the NSOperationQueue before adding your latest search operation to the queue.

     [self.searchQueue cancelAllOperations];

Any yet to execute operations will be cancelled and never execute. Any presently executing operation will run to completion. In a more complex situation, perhaps with a multistep NSOperation, (where you were using a custom subclass of NSOperation or something) you could check for isCanceled on NSOperaton instance and bail out of your operation early. I’ll leave that up to you to figure out.

So, with this change the code becomes:

 - (BOOL)searchDisplayController:(UISearchDisplayController *)controller 
shouldReloadTableForSearchString:(NSString *)searchString
{
   if  ([searchString isEqualToString:@""] == NO)
    {
        [self.searchQueue cancelAllOperations];
        [self.searchQueue addOperationWithBlock:^{
            
            NSArray *results = // fetch the results from
              // somewhere (that can take a while to do)
            
            // Ensure you're assigning to a local variable here.
            // Do not assign to a member variable.  You will get
            // occasional thread race condition related crashes 
            // if you do.            

            [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                // Modify your instance variables here on the main
                // UI thread.
                [self.searchResults removeAllObjects]; 
                [self.searchResults addObjectsFromArray:results];
                
                // Reload your search results table data.
                [controller.searchResultsTableView reloadData];
            }];
        }];
        
        return NO;
    }
    else
    {
        [self.searchResults removeAllObjects];
        return YES;
    }
}

One final little thing, it is probably a good idea to cancel any pending operations when the user dismisses the search UI.

- (void)searchDisplayControllerWillEndSearch:(UISearchDisplayController *)controller
{
    [self.searchQueue cancelAllOperations];
}

You may wish to cancel any pending operations in other delegate methods too. Such as tableView:didSelectRowAtIndexPath: for example.

Advertisements

5 thoughts on “Cancellable asynchronous searching with UISearchDisplayController

  1. Pingback: thetawelle » Blog Archiv » Non-Blocking NSString-search using e.g. UITextField & UISearchViewController - frontier of science, technology & future

  2. You have no idea how happy your solution made me. I had an in memory array to search that was so jerky because a) it is 800,000 items and b) need to use a predicate with so many OR’s as I need to search many of my object’s properties.

    Using your code made it so usable

    Thank you

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s