The default layout manager fills in the background color (specified via NSAttributedString .backgroundColor attribute) where there's no text (except for the last line).
I've managed to achieve the effect I want by sublclassing NSLayoutManager and overriding func drawBackground(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint)
as follows:
override func drawBackground(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) {
guard let textContainer = textContainers.first, let textStorage = textStorage else { fatalError() }
// This just takes the color of the first character assuming the entire container has the same background color.
// To support ranges of different colours, you'll need to draw each glyph separately, querying the attributed string for the
// background color attribute for the range of each character.
guard textStorage.length > 0, let backgroundColor = textStorage.attribute(.backgroundColor, at: 0, effectiveRange: nil) as? UIColor else { return }
var lineRects = [CGRect]()
// create an array of line rects to be drawn.
enumerateLineFragments(forGlyphRange: glyphsToShow) { (_, usedRect, _, range, _) in
var usedRect = usedRect
let locationOfLastGlyphInLine = NSMaxRange(range)-1
// Remove the space at the end of each line (except last).
if self.isThereAWhitespace(at: locationOfLastGlyphInLine) {
let lastGlyphInLineWidth = self.boundingRect(forGlyphRange: NSRange(location: locationOfLastGlyphInLine, length: 1), in: textContainer).width
usedRect.size.width -= lastGlyphInLineWidth
}
lineRects.append(usedRect)
}
lineRects = adjustRectsToContainerHeight(rects: lineRects, containerHeight: textContainer.size.height)
for (lineNumber, lineRect) in lineRects.enumerated() {
guard let context = UIGraphicsGetCurrentContext() else { return }
context.saveGState()
context.setFillColor(backgroundColor.cgColor)
context.fill(lineRect)
context.restoreGState()
}
}
private func isThereAWhitespace(at location: Int) -> Bool {
return propertyForGlyph(at: location) == NSLayoutManager.GlyphProperty.elastic
}
However, this doesn't handle the possibility of having multiple colors specified by range in the attributed string. How might I achieve this? I've looked at fillBackgroundRectArray
with little success.
Heres' how I reached your goal to highlight the " sit " term in different colors inside the famous
Lorem ipsum...
that is huge enough to test on multiple lines.All the basics that support the following code (Swift 5.1, iOS 13) are provided in this answer and won't be copied here for clarity reason ⟹ they led to the result 1 hereafter.
In your case, you want to highlight some specific parts of a string which means that these elements should have dedicated key attributes due to their content ⟹ in my view, it's up to the
textStorage
to deal with it.MyTextStorage.swift
If you build and run from here, you get the result 2 that shows a problem with each colored background of " sit " found in the text ⟹ there's an offset between the
lineFragment
and the colored background rectangles.I went and see the
fillBackgroundRectArray
method you mentioned and about which Apple states that it 'fills background rectangles with a color' and 'is the primitive method used bydrawBackground
': seems to be perfect here to correct the layout problem.MyLayoutManager.swift
Parameters adjustments need to be explored in depth to have a generic formula but for the example it works fine as is.
Finally, we get the result 3 that enables the possibility of having multiple colors specified by range in the attributed string once the conditions of the regular expression are adapted.