Reputation: 16002
I want to be able to add custom underlines to some parts/words only of a multiline label.
I tried using NSAttributedString.Key.underlineStyle
with a NSMutableAttributedString
, but I don't have access to important parameters such as the spacing between the text and the underline, or the underline bar height.
So I was thinking of adding a sublayer to the UILabel
below the expected text. Here is what I have done so far:
extension UILabel {
func underline(word: String, withColor color: UIColor) {
guard let text = self.text else {
return
}
if let range = text.range(of: word) {
let start = text.distance(from: text.startIndex, to: range.lowerBound)
let end = text.distance(from: text.startIndex, to: range.upperBound)
if let rect = self.boundingRectFor(characterRange: start..<end) {
let c = CALayer()
c.frame = CGRect(origin: CGPoint(x: rect.origin.x, y: rect.size.height + (self.font.pointSize / 8)), size: CGSize(width: rect.size.width, height: (self.font.pointSize / 4)))
c.backgroundColor = color.cgColor
self.layer.addSublayer(c)
}
}
}
private func boundingRectFor(characterRange: Range<Int>) -> CGRect? {
guard let attributedText = attributedText else { return nil }
let textStorage = NSTextStorage(attributedString: attributedText)
let layoutManager = NSLayoutManager()
textStorage.addLayoutManager(layoutManager)
let textContainer = NSTextContainer(size: intrinsicContentSize)
textContainer.lineFragmentPadding = 0.0
layoutManager.addTextContainer(textContainer)
var glyphRange = NSRange()
layoutManager.characterRange(forGlyphRange: NSRange(location: characterRange.startIndex, length: characterRange.endIndex - characterRange.startIndex), actualGlyphRange: &glyphRange)
return layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
}
}
boundingRectFor(characterRange)
is a method I have found that is expected to return the rect containing the substring from the range passed as the parameter.
underline(word: String, withColor color: UIColor)
will look for the first occurrence of the given word in the label text and underline it.
Simple use in a view controller:
class ViewController: UIViewController {
@IBOutlet var labels: [UILabel]!
var texts: [String] = ["Lorem ipsum dolor sit amet.",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."]
var textToUnderline = ["ipsum", "amet", "incididunt"]
var underliningColors: [UIColor] = [.red, .blue, .purple]
@IBOutlet weak var firstLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
self.labels.enumerated().forEach { index, label in
label.text = texts[index]
label.underline(word: textToUnderline[index], withColor: underliningColors[index])
}
}
}
And here is a screenshot of the result:
Although this works fine when the first line is concerned, I doesn't work anymore when trying to underline another line.
To have a better understanding, I tried to print some values below if let rect
(in the UILabel
extension):
// will print the label frame and the rect to underline
print("frame: \(self.frame) and rect: \(rect)")
And here is what I get:
frame: (24.0, 68.0, 366.0, 0.0) and rect: (34.0576171875, 0.0, 30.3369140625, 11.93359375)
frame: (24.0, 100.0, 366.0, 0.0) and rect: (176.35009765625, 0.0, 40.375, 20.287109375)
frame: (24.0, 124.0, 366.0, 0.0) and rect: (572.20458984375, 0.0, 71.14013671875, 17.900390625)
I realized that the origin.x
of the last label rect (572.2...) exceeds its width (366), but I am struggling to understand why I am getting this result and how I can improve my function.
Thank you for your help.
Upvotes: 0
Views: 499
Reputation: 535944
Your entire approach is wrong. Use a text view, not a label, so that you have access to the full text kit stack (NSLayoutManager, NSTextStorage, NSTextContainer). Construct the stack using a custom layout manager subclass and override drawBackground(forGlyphRange)
.
Upvotes: 3