NSOutlineView Searching trees

Searching trees

Introduction:

Searching a tree may seem like a complicated task, but there really is not much to it 
In this post, I will be talking about filtering an abstract hierarchy of objects arranged in a tree. Here, a number of solutions is presented to this rather trivial problem. In all cases, we are dealing with a hierarchy of nodes, each of them containing just the  name field for simplicity.
When searching a tree one has two options: linearize a tree or traverse it recursively. 
In case of an abstract structure, such as a class hierarchy, the tree is close to an array, at least from the perspective of searching. In fact, plucking every node in the tree into a linear array and searching the array is just as fast as traversing the tree. Except for the fact that it kills the hierarchal nature of the tree. However, in case of the class structure, we are not extremely concerned with it since each element implicitly contains the hierarchy within itself.
Another option is to recursively traverse the tree, recording the search hits into an external set or array. This will then allow to show the structure in the result of the search.
Here is the implementation of those cases: 

Linear structure

We are dealing with one managed object context that does most of the magic for us. All the nodes and their respective fields are born and managed inside this context. We should just be concerned with the fact that it maintains the tree. All the details (or most, in any case) are behind a curtain.
The controller layer involves one  NSTreeController, and two NSArrayControllers, as shown in the figure to the left. As the names suggest, one of the array controllers manages the top-level (root) search, while the other involves the entire tree. The components controller takes care of the unsorted hierarchy of objects. We also have the application delegate which is the center relay point for communication between the components.  ManagedObjectContext of all the controllers is wired to the Application  delegate's context, which is the data model of the application. Both of the array controllers manage entities of the class Node. The second figure shows the setting for the parent level controller. The only difference between the two array controllers is that the recursive search controller has no fetch predicate, which defaults to all the values of the object context.
The actual interface has an  NSOutlineView, and a search box with a button (-r toggle) that switches between the search modes (recursive vs. parent), as in the figure below. There is also an  NSTableView that shows the results of the search. It is programatically wired to become invisible whenever the search box is empty. For debugging purposes, I have add and remove buttons that allow me to manage the tree. The  value binding of the outline view is connected to the  arrangedObjects.name of the tree controller, so it shows the names of the nodes from the object model.

There is a trick in switching between the views programatically. Essentially, we have to  bind the  hidden property of each of the view to an internal boolean in the application delegate code. Then we can switch it on demand. Here is the code.
- (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);
}
@end
The 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

The second part of the post deals with a navigation problem. We want to show the path to a class in the hierarchy by expanding all the nodes that lead towards it. We can think of it as a sort of breadcrumbs thing.
All this can be comfortably managed inside a subclass of a  NSTreeController.
The XIB interface looks exactly the same in this case, but we are not using any table views, just an outline view that we expand the nodes in.

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.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值