NSLayoutManager boundingRectForGlyphRange off by s

2019-02-23 01:28发布

问题:

I want to "highlight" specific words in UITextView, not with a solid color (which would be possible via NSAttributedString), but rather with a gradient and maybe some other fancy effects.

Hence, I decided it would be necessary to manually create a view and overlay (or underlay) it using the bounding rectangles of the given text.

To my surprise this turned out to be pretty straightforward. Example is inside a UIViewController, txtView being a UITextView connected as IBOutlet:

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    // Converting to NSString as I need a NSRange for glphyRangeForCharacterRange
    // (a normal String in Swift returns a Range)
    let charRange = (txtView.text as NSString).rangeOfString("dolor sit er elit")
    assert(charRange.location != NSNotFound, "Character Range could not be found.")

    let glyphRange = txtView.layoutManager.glyphRangeForCharacterRange(charRange,
        actualCharacterRange: nil)
    let glyphContainer = txtView.layoutManager.textContainerForGlyphAtIndex(glyphRange.location, effectiveRange: nil)
    let glyphRect = txtView.layoutManager.boundingRectForGlyphRange(glyphRange,
        inTextContainer: glyphContainer!)

    let highlightView = UIView(frame: glyphRect)
    highlightView.alpha = 0.3
    highlightView.backgroundColor = UIColor.redColor()
    txtView.addSubview(highlightView)
}

Sadly, this leads to the overlain view (in red in the below screenshot), being off by a few points.

It always seems to be off by the same amount. Which would mean I could probably hardcode, but that has never been a good idea. Tested this in Simulator with various devices and on an iPhone 6 with iOS 8.1.3.

I also tried other variations (by creating my own NSTextContainer etc.) as inspired by the question on how to get a CGRect for a substring and "boundingRectForGlyphRange does not always work for all strings"

Any ideas (besides resorting to CoreText)?

回答1:

Try adjusting the glyph rect by the textview's textContainerInset:

var glyphRect = txtView.layoutManager.boundingRectForGlyphRange(glyphRange,
        inTextContainer: glyphContainer!)

glyphRect.origin.y += txtView.textContainerInset.top

That should do the trick!