Create NSTextView with Facebook-like Tags/Mentions

antaggedtextview

I have been working on an OS X app related to time planning and I needed to provide a text input where users can tag or mention places. My biggest challenge was to actually render those mentions and tags with other regular text.

In this post, I will explain how I built my version of NSTextView (second text box in the screenshot above). However, the full implementation offers NSTextField and NSTextFieldCell implementations as well which can be used for other cases, such as inside NSTableView:

antaggedtextview_example3

We will be utilizing NSAttributedString for this. If you are not familair with NSAttributedString, it’s quite easy to understand and a powerful version of NSString. You can basically have “attributes” on any substring (including the entire string), think of those attributes as HTML tags. There are various built-in attributes which you can use to change text color, style, paragraph style, hyperlinks and various other things. However, a rounded and bordered background color is not one of them :). But worry not, we will be able to implement our custom attribute very soon (if you’re too lazy, feel free to simply use my full implementation)

1. Subclass NSTextView

First, we need to subclass NSTextView and override drawRect. We will implement the logic to draw our custom attribute here:

- (void)drawRect:(NSRect)dirtyRect {
 [super drawRect:dirtyRect];
}

This simply will cause our subclass to behave exactly as a typical NSTextView would do. Which is a good thing because it means we only have to implement drawing logic for tagged substrings.

Find tags/mentions

We will use  - enumerateAttributesInRange:options:usingBlock: to iterate through attributes in the string. Whenever we encounter “Tag” attribute, we will draw our rounded box and draw the substring above it. The biggest challenge I encountered here is to calculate the exact position of the text to draw it right above the text drawn by the super class’ drawRect. Anyway, long story short:

- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
[self.attributedString enumerateAttributesInRange:(NSRange){0, self.string.length} options:NSAttributedStringEnumerationReverse usingBlock:
^(NSDictionary *attributes, NSRange range, BOOL *stop) {
if ([attributes objectForKey:@"Tag"] != nil)
{
NSDictionary* tagAttributes = [self.attributedString attributesAtIndex:range.location effectiveRange:nil];
NSSize oneCharSize = [@"a" sizeWithAttributes:tagAttributes];
NSRange activeRange = [self.layoutManager glyphRangeForCharacterRange:range actualCharacterRange:NULL];
NSRect tagRect = [self.layoutManager boundingRectForGlyphRange:activeRange inTextContainer:self.textContainer];
tagRect.origin.x += self.textContainerOrigin.x;
tagRect.origin.y += self.textContainerOrigin.y;
tagRect = [self convertRectToLayer:tagRect];
NSRect tagBorderRect = (NSRect){ (NSPoint){tagRect.origin.x-oneCharSize.width*0.25, tagRect.origin.y+1}, (NSSize){tagRect.size.width+oneCharSize.width*0.5, tagRect.size.height} };

[NSGraphicsContext saveGraphicsState];
NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:tagBorderRect xRadius:3.0f yRadius:3.0f];
NSColor* fillColor = [NSColor colorWithCalibratedRed:237.0/255.0 green:243.0/255.0 blue:252.0/255.0 alpha:1];
NSColor* strokeColor = [NSColor colorWithCalibratedRed:163.0/255.0 green:188.0/255.0 blue:234.0/255.0 alpha:1];
NSColor* textColor = [NSColor colorWithCalibratedRed:37.0/255.0 green:62.0/255.0 blue:112.0/255.0 alpha:1];

[path addClip];
[fillColor setFill];
[strokeColor setStroke];
NSRectFillUsingOperation(tagBorderRect, NSCompositeSourceOver);
NSAffineTransform *transform = [NSAffineTransform transform];
[transform translateXBy: 0.5 yBy: 0.5];
[path transformUsingAffineTransform: transform];
[path stroke];
[transform translateXBy: -1.5 yBy: -1.5];
[path transformUsingAffineTransform: transform];
[path stroke];

NSMutableDictionary* attrs = [NSMutableDictionary dictionaryWithDictionary:tagAttributes];
NSFont* font = [tagAttributes valueForKey:NSFontAttributeName];
font = [[NSFontManager sharedFontManager] convertFont:font toSize:[font pointSize] - 0.25];
[attrs addEntriesFromDictionary:@{NSFontAttributeName: font, NSForegroundColorAttributeName: textColor}];
[[self.attributedString.string substringWithRange:range] drawInRect:tagRect withAttributes:attrs];

[NSGraphicsContext restoreGraphicsState];
}
}];
}

The code is mostly self-explanatory, here are some highlights thoguh:

  1. [layoutManager boundingRectForGlyphRange:activeRange inTextContainer:self.textContainer]returns the rectangular that we should lay our text in.
  2. tagBorderRect is a copy of tagRect but it extends about 1/4 characater from the left and 1/2 characater on the right side.

2. Use the new NSTextView

Now you can simply use the NSTextView subclass by dragging and dropping an NSTextView onto your View in Interface Builder or Storyboard and then set the Custom Class to your subclass name (in my case, I called it ANTaggedTextView):

antaggedtextview_example

3. Enjoy

Now we’re ready to tag some text using the “Tag” attribute. Bind the text view to an outlet, and set some content as follows:

NSMutableString* content = [NSMutableString stringWithString:@"Hello World!\n"];
[content addAttribute:@"Tag" value:@1 range:[content.string rangeOfString:@"World"]];
[self.textView.textStorage setAttributedString:content];

Full Source Code

Full source code is available at: https://github.com/aimannajjar/ANTaggedTextView with additional NSTextField and NSTextFieldCell implementations that can be used either standalone or as part of view-based NSTableView. The usage is documented in the README file.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s