Confused Development

I develop software and I often get confused in the process. I usually find the answers after a while, but a month later I can't remember them. So from now on, I will write them down here.

Thursday, October 19, 2006

Waiting for a Spotlight Query to Finish (Spotlight and autocomplete)

After a long time of doing other things, I finally got around to working on semiBlog again. In that context, I was playing with integrating Spotlight (through NSMetadataQuery). Now, NSMetadataQueries are asynchronous, i.e. you create your query, you register a call-back method for when then query gives some results, and then you run the query. In other words, after the query starts running, the code doesn't halt and wait for the query to finish - instead, it just continues. Then, whenever the query returns some results, the call-back method is called. Looks like this:

- (void)someMethod
{
    NSMetadataQuery *query = [[NSMetadataQuery alloc] init];
    NSPredicate *pred = [NSPredicate predicateWithFormat: queryString]; 
    [query setPredicate: pred];
    
    [[NSNotificationCenter defaultCenter]
             addObserver: self
                selector: @selector(queryHandler:)
                    name: NSMetadataQueryDidFinishGatheringNotification
                  object: query];
    
    [query startQuery];

    // whatever comes now happens immediately:
    NSLog(@"something happens");
}

- (void)queryHandler: (NSNotification *) inNotification
{
     NSLog(@"The query is finished, do something.");
     // blabla...
}

So far, so good. Now, what I want to do in semiBlog is integrate such a Spotlight query in the autocomplete of an NSTextView. I can manipulate the autocomplete suggestions of NSTextView by overriding completionsForPartialWordRange:indexOfSelectedItem: (or by calling textView:completions:forPartialWordRange:indexOfSelectedItem: in the delegate). Whenever autocomplete is initiated, these methods are called. The NSArray they return determines what will show up in the list of completion suggestions. Looks like this:

- (NSArray *)completionsForPartialWordRange: (NSRange)charRange 
indexOfSelectedItem: (int *)index
{
   return [NSArray arrayWithObjects: @"eins", @"zwei", @"drei", nil];
}
Basic autocomplete Of course, when I want to use Spotlight here (e.g. I type "Knud", initiate autocomplete, Spotlight finds all contacts and events that somehow match the string "Knud", I use the result set for the completion suggestions), I run into problems. I can initiate the query in this method, but I will not get the result here, so that I can construct the return array from it. Instead, the result is handled in the asynchronous call-back. So, what to do? After a bit of searching, I found that run loops are what is needed here (see the documentation for CFRunLoops here). In short, from a current run loop I have to start a new run loop right after I initiate the query (calling CFRunLoopRun()). The current will then wait until the new loop finishes (calling CFRunLoopStop(CFRunLoopGetCurrent ())), which I will let it do at the end of the call-back, after the query results have been processed. So, now the code looks a little like this:

- (NSArray *)completionsForPartialWordRange: (NSRange)charRange 
indexOfSelectedItem: (int *)index
{
 // get the current word. That will be the keyword in our query
 NSString *word = [[self string] substringWithRange: charRange];
 
 // construct the query string from this word:
 NSString *queryString = @"someQueryString";
 
 // construct the objects needed to perform the query
 NSMetadataQuery *query = [[NSMetadataQuery alloc] init];
 NSPredicate *pred = [NSPredicate predicateWithFormat: queryString];
 [query setPredicate: pred];
 
        // register the call-back
 [[NSNotificationCenter defaultCenter]
             addObserver: self
    selector: @selector(queryHandler:)
     name: NSMetadataQueryDidFinishGatheringNotification
      object: query];
 
 [query startQuery];
 
        //start a new run loop
 CFRunLoopRun();
 
        // after the new loop is finished, continue by returning the new suggestions:
 return [self suggestions];
}

- (void)queryHandler: (NSNotification *) inNotification
{
 NSMetadataQuery *query = [inNotification object];

 NSArray *suggestions = // create the suggestions array from the query result
 
 [self setSuggestions: suggestions];
 
        // stop the new run loop
 CFRunLoopStop(CFRunLoopGetCurrent ());
}
Autocomplete based on a Spotlight query

Now, I'm not sure I have grasped this run loop business completely - maybe there are some pitfalls here - , but the code seems to work! :-)