Search code examples
iphoneiosregexuitextviewnsregularexpression

Suggest # tags while typing (like Twitter) for iPhone UITextView


I'd building an app that uses hashtags, like Twitter or Tweetbot. When you're typing a message, if you type the hashtag symbol, I'd like to suggest tags that match the current one you're typing.

I've already figured out how to get the UITableView to appear and show a list of hashtags, but what I can't figure out is how to do the following:

  1. Get the NSRange of the current word being typed,
  2. See if that range is formatted like a hashtag (NSRegularExpression @"#\\w\\w*")
  3. (From here on out, I've got the code figured out to search for matching hashtags and show them in the UITableView)

Can anyone help me with steps 1 and 2? I've been thinking about using textViewDidChange:, but I'm concerned that the app's performance might suffer if I'm constantly running methods every time the characters change.

Thanks!


Solution

  • I figured it out! I wound up using the textViewDidChange: and textViewDidChangeSelection: methods.

    To get the NSRange of the current hashtag being typed, I ran a for loop over the NSRegularExpression matches in the text string. From there, I used NSLocationInRange to find out if the current cursor position intersected any of the hashtags.

    Here's the code:

    //Get the ranges of current hashtags
    NSArray *hashtagRanges = [StringChecker rangesOfHashtagsInString:textView.text];
    NSTextCheckingResult *currentHashtag;
    
    if ([hashtagRanges count] >0)
    {
        //List the ranges of all the hashtags
        for (int i = 0; i<[hashtagRanges count]; i++) 
        {
            NSTextCheckingResult *hashtag = [hashtagRanges objectAtIndex:i];
            //Check if the currentRange intersects the hashtag
            //Have to add an extra space to the range for if you're at the end of a hashtag. (since NSLocationInRange uses a < instead of <=)
            NSRange currentlyTypingHashtagRange = NSMakeRange(hashtag.range.location, hashtag.range.length + 1);
            if (NSLocationInRange(currentRange.location, currentlyTypingHashtagRange))
            {
                //If the cursor is over the hashtag, then snag that hashtag for matching purposes.
                currentHashtag = hashtag;
            }
        }
    
        if (currentHashtag){
            //If we found one hashtag that we're currently editing
    
            //Display the hashtag suggester, feed it the current hashtag for matching.
            [self showTagTable];
    
            //Get the current list of hashtags into an array
            NSFetchRequest *hashtagRequest = [[NSFetchRequest alloc] init];
            NSEntityDescription *tagEntityDescription = [NSEntityDescription entityForName:@"Tags" 
                                                                    inManagedObjectContext:self.note.managedObjectContext];
            [hashtagRequest setEntity:tagEntityDescription];
            NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"dateLastUsed" 
                                                                             ascending:YES];
            NSArray *sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
            [hashtagRequest setSortDescriptors:sortDescriptors];
    
            NSPredicate *tagPredicate = [NSPredicate predicateWithFormat:@"name contains[c] %@", [noteTextView.text substringWithRange:currentHashtag.range]];
            [hashtagRequest setPredicate:tagPredicate];
    
            tagsToDisplay = (NSMutableArray *)[self.note.managedObjectContext executeFetchRequest:hashtagRequest error:nil];
            [tagListTable reloadData];
    
            //If there are no matching hashtags, then let's hide the tag table.
            if ([tagsToDisplay count] == 0) 
            {
                [self hideTagTable];
                return;
            }
    
        }
    

    The StringChecker class is a custom one that I wrote, it just has class methods that parse the strings. I made StringChecker a class because the methods are used in several places in the app. Here's the method:

        #pragma mark - Hashtag Methods
    +(NSArray *)rangesOfHashtagsInString:(NSString *)string {
        NSRegularExpression *hashtagDetector = [[NSRegularExpression alloc] initWithPattern:@"#\\w\\w*" 
                                                                                    options:NSRegularExpressionCaseInsensitive 
                                                                                      error:nil];    
        NSArray *hashtagRanges = [hashtagDetector matchesInString:string
                                                          options:NSMatchingWithoutAnchoringBounds
                                                            range:NSMakeRange(0, string.length)];
        return hashtagRanges;
    }
    
    +(NSUInteger)numberOfHashtagsInString:(NSString *)string {
        NSRegularExpression *hashtagDetector = [[NSRegularExpression alloc] initWithPattern:@"#\\w\\w*" 
                                                                                    options:NSRegularExpressionCaseInsensitive 
                                                                                      error:nil];
        NSUInteger numberOfHashtags = [hashtagDetector numberOfMatchesInString:string
                                                                       options:NSRegularExpressionCaseInsensitive
                                                                         range:NSMakeRange(0, string.length)];
        return numberOfHashtags;
    }