I thought I would share a bit of code with anyone out there who wonders exactly how basic searching is achieved in your favorite applications.
Conceptually, the user will enter a search string in the search box, something like “Jo”. Now, you will need to find every object in your table that has “jo” in it. You get to choose the amount of control, as a programmer, you put into this. If you want to be very kind, you can add a drop-down menu that the user clicks to search for “Jo” just in the name field, for example. However, I feel that most people are comfortable enough with just filtering (ie, not full-scale searching), we will simply search every item in our object to find matches. Still with me?
For example (’Por Exemplo’ comes to mind..) :
Imagine we are creating an address book-like app (hint-hint) and each person in our address book has four pieces of data each. Each person has a name, a screen name, an url, and an image associated with them. We certainly can’t filter the images, but we would like to search through the names, screen names, and urls. So, let’s figure out what we’re going to do.
First, you’re going to setup your interface in IB. I’m not going to take the time to go through all of this, but you drop a NSSearchField onto your window and hook up the bindings with the “predicate” option farther down the Inspector window. I have an array controller of “Person” objects, so I want to set my bindings as “PeopleController” as my source (same as the data source for my tableview), filterPredicate, and the model key as “personName” (the string that holds the name in a Person object, doesn’t really matter, but you must fill in a valid KVC value here).
I have also subclassed out the standard NSArrayController to provide filtering support. I connect my “search” IBAction to my NSSearchField, to actually implement the searching. I also like the instant filtering style, so I select the “Sends Search String Immediately” attribute in IB for my NSSearchField.
That ties up the interface stuff, now we need to actually figure out how we are going so sort. The concept behind this is we go through every object in the original array (that supplies the table) and see if it matches our search criteria. If it does match, we’ll throw it into a new array. When we’ve searched everything, we’ll return this array to the table so it can display it as the search results. When the user is done searching, we release that array and return the table back to the original array that holds all of our data. Let’s start searching!
Code for the filtering Array Controller
- (void)search:(id)sender {
[self setSearchString:[sender stringValue]];
[self rearrangeObjects];
}
- (NSArray *)arrangeObjects:(NSArray *)objects {
if ((searchString == nil) || ([searchString isEqualToString:@”"])) {
newObject = nil;
return [super arrangeObjects:objects];
}
/*
Create array of objects that match search string.
Also add any newly-created object unconditionally:
(a) You’ll get an error if a newly-added object isn’t added to arrangedObjects.
(b) The user will see newly-added objects even if they don’t match the search term.
*/
NSMutableArray *matchedObjects = [NSMutableArray arrayWithCapacity:[objects count]];
// case-insensitive search
NSString *lowerSearch = [searchString lowercaseString];
NSEnumerator *oEnum = [objects objectEnumerator];
id item;
while (item = [oEnum nextObject]) {
// if the item has just been created, add it unconditionally
if (item == newObject) {
[matchedObjects addObject:item];
newObject = nil;
} else {
NSString *lowerName = [[item valueForKeyPath:@”personName”] lowercaseString];
NSString *lowerScreenName = [[item valueForKeyPath:@”personScreenName”] lowercaseString];
NSString *lowerUrl = [[item valueForKeyPath:@”personUrl”] lowercaseString];
if ( lowerName != NULL && [lowerName rangeOfString:lowerSearch].location != NSNotFound) {
[matchedObjects addObject:item];
}
else if ( lowerScreenName != NULL && [lowerScreenName rangeOfString:lowerSearch].location != NSNotFound ) {
[matchedObjects addObject:item];
}
else if ( lowerUrl != NULL && [lowerUrl rangeOfString:lowerSearch].location != NSNotFound ) {
[matchedObjects addObject:item];
}
}
}
return [super arrangeObjects:matchedObjects];
}
The code above goes through each object in the array and pulls out a string for the name, url, and screen name. It compares the entered search string to each of those strings, and if it matches, adds it to the array we return that has the matchedObjects. Of note here is that we check for a NULL string from the object because if we don’t, the object will show up in the results even though it shouldn’t.
I think that was enough of an intro to searching. Let me know if you would like to hear more about implementing the ‘Recent Search Items’ list, or searching by category. Comments are welcome. If you’re interested in a working XCode project with the code, let me know and I might throw one together and post it online.
UPDATE: I didn’t originally provide enough information about this controller, so here are a few links and documentation. Apple has documentation on this subject with almost the identical sample code that is published by mmalc below. You can read the documentation here.
Download mmalc’s source (pretty much what is in the documenation) here. This is a sample project, so it’s nice to look at as well. Of note, if you want the iTunes, Spotlight-like behavior of instant results (a true “filtering” action, vs searching), go into Interface Builder, select the NSSearchField and for it’s attributes, check the box with “Sends search string immediately”. A nice example. Check out his website with lots of other good bindings related examples is here.