It requires one barely-documented bit of magic and another completely undocumented bit. Here's Objective-C code. Don't have it handy in Swift, sorry.
NSRect textBounds = [textField.cell titleRectForBounds:textField.bounds];
NSTextContainer* textContainer = [[NSTextContainer alloc] init];
NSLayoutManager* layoutManager = [[NSLayoutManager alloc] init];
NSTextStorage* textStorage = [[NSTextStorage alloc] init];
[layoutManager addTextContainer:textContainer];
[textStorage addLayoutManager:layoutManager];
textContainer.lineFragmentPadding = 2;
layoutManager.typesetterBehavior = NSTypesetterBehavior_10_2_WithCompatibility;
textContainer.containerSize = textBounds.size;
[textStorage beginEditing];
textStorage.attributedString = textField.attributedStringValue;
[textStorage endEditing];
NSUInteger count;
NSRectArray rects = [layoutManager rectArrayForCharacterRange:matchRange
withinSelectedCharacterRange:matchRange
inTextContainer:textContainer
rectCount:&count];
for (NSUInteger i = 0; i < count; i++)
{
NSRect rect = NSOffsetRect(rects[i], textBounds.origin.x, textBounds.origin.y);
rect = [textField convertRect:rect toView:self];
// do something with rect
}
The typesetterBehavior
is documented here. The lineFragmentPadding
was determined empirically.
Depending on exactly what you're planning to do with the rectangles, you may wish to pass { NSNotFound, 0 }
as the selected character range.
For efficiency, you generally want to keep the text objects around instead of instantiating them every time. You just set the text container's containerSize
and the text storage's attributedString
to the appropriate values each time.