Introduction:
![1.png](https://lindsay.cpsc.ucalgary.ca/users/ikryukov/weblog/1cd84/images/3d9a8.png)
Linear structure
![2.png](https://lindsay.cpsc.ucalgary.ca/users/ikryukov/weblog/1cd84/images/e0411.png)
![4.png](https://lindsay.cpsc.ucalgary.ca/users/ikryukov/weblog/1cd84/images/c0851.png)
![3..png](https://lindsay.cpsc.ucalgary.ca/users/ikryukov/weblog/1cd84/images/64c88.png)
- (IBAction)switchSearchResultView:(id)sender { if (![[sender stringValue] isEmpty]) { [self hideHierarchyView]; } if ([[sender stringValue] isEmpty]) { [self hideSearchView]; } }isEmpty is a category method of NSString, the implementation of which looks like this:
@implementation NSString (Empty) -(BOOL) isEmpty { return ([self length] == 0); } @endThe search inside the array controllers is managed with bindings of the fetch predicate. We need to alter those depending on the toggle by the user (recursive vs. parent level), so they have to be hardcoded. Here is the method that is called when the toggle (-r) is clicked:
- (IBAction)switchRecursiveSearch:(id)sender { //generate a predicate //"match anything that contains search query, ignore case" NSPredicate* searchPredicate = [NSPredicate predicateWithFormat:@"name contains[c] $value"]; //generate a dictionary to be passed as a binding option list NSDictionary* searchPredicateOptions = [NSDictionary dictionaryWithObject:searchPredicate forKey:@"Predicate Format"]; //if recursive toggle button state is true: if ([sender state]) { //bind the search box's predicate to an array controller with options [searchBox bind:@"predicate" toObject:recursiveSearchController withKeyPath:@"filterPredicate" options:searchPredicateOptions]; //make the table view column show //the names under the array controller [searchResultsColumn bind:@"value" toObject:recursiveSearchController withKeyPath:@"arrangedObjects.name" options:nil]; } //if false if (![sender state]) { //by analogy [searchBox bind:@"predicate" toObject:parentSearchController withKeyPath:@"filterPredicate" options:searchPredicateOptions]; [searchResultsColumn bind:@"value" toObject:parentSearchController withKeyPath:@"arrangedObjects.name" options:nil]; } }
The code is rather self-explanatory, but it takes some time to find in the documentation.
Essentially, the filter predicate takes care of all the search magic, so we don't have to worry about it. Also, the predicate class provides a lot of versatility in terms of searching, so that we are able to fine-tune performance. However, this binding is not available on NSTreeController. This means that a bit more trickery would be needed to search a tree preserving the hierarchy.
Traversing a tree
![5.png](https://lindsay.cpsc.ucalgary.ca/users/ikryukov/weblog/1cd84/images/30f79.png)
We would need a recursive traversal method that will look for matches and add them into a mutable array. In this case, since we are working with expanding nodes in the class hierarchy, it is convenient to use NSIndexPath values of the nodes. This way, we can expand the desired nodes in a call like this:
[NSTreeController setSelectionIndexPaths:searchResults] [hierarchyView expandItem:[filteredHierarchy selectionIndexPaths]]Where (obviously) the class names would be replaced with references to the objects. Search results is the mutable array of path indices.
Following is the implementation of the extended tree controller that recursively goes through the tree, and expands it to the point of found items.
The search algorithm would something as simple as:
- (void) search:(NSString *)query inSection:(NSArray *)selection { //iterate through the roots for (NSTreeNode* item in selection) { //fetch children NSArray* children = [item childNodes]; if ([children count] > 0) { //get a level deeper if you have children [self search:query inSection:children]; } //get current name NSString* currentName = [[item representedObject] valueForKey:@"name"]; //figure out if it matches the query NSRange cat = [currentName rangeOfString:query]; BOOL match = (cat.location != NSNotFound); //add to record if matched if (match) { [searchResults addObject:[item indexPath]]; } } }
Again, this method traverses the tree, and records all the found items in a mutable array.This method is in terms called from:
- (void) filterContentBy:(NSString *)query { //get a clean array searchResults = [NSMutableArray array]; //fill it in [self search:query inSection:[[self arrangedObjects] childNodes]]; //if it now containes results filtered = [searchResults count] > 0; if (filtered) { //select them in the content of the tree controller [self setSelectionIndexPaths:searchResults]; } }
After filling in the result mutable array, we have to check if the search found anything. If it did, we can set the selection to the found nodes. This action is called from a method that checks if the search box is empty. If it is not, we expand the hierarchy to the values found with the query currently entered in the search box. Notice that for a smooth action, we have to call this method whenever a new symbol is typed in a search box, not only on enter. This option is configured in the attributes inspector of the search field. The results method looks like this:
- (IBAction) revealFoundItems:(id)sender { //get the string NSString* query = [sender stringValue]; //if it is empty, clear the search results if ([query isEmpty]) { [hierarchyView collapseItem:nil collapseChildren:YES]; //collpasing nil defaults to the root } //or reveal the search results else { [filteredHierarchy filterContentBy:query]; [hierarchyView expandItem:[filteredHierarchy selectionIndexPaths]]; } //if the tree controller did not find anything,clear the results //this method is called in a case like: //query: a --> match //user keeps typing: ab --> no match --> clear results if (![filteredHierarchy filtered]) { [hierarchyView collapseItem:nil collapseChildren:YES]; } }
Notice that we need to clear the results of a search in two cases. First, if the user enters nothing in the search field. Second is when none of the hierarchy nodes match the search query. This gives the search a bit of a procedural nature, since we need to check the tree every time a new query is entered.