I have a UILabel subclass showing NSAttributedString in which I need to draw a rounded rectangle background color around links:
import UIKit
class MyLabel: UILabel {
private var linkRects = [[CGRect]]()
private let layoutManager = NSLayoutManager()
private let textContainer = NSTextContainer(size: .zero)
private let textStorage = NSTextStorage()
override func draw(_ rect: CGRect) {
let path = UIBezierPath()
linkRects.forEach { rects in
rects.forEach { linkPieceRect in
path.append(UIBezierPath(roundedRect: linkPieceRect, cornerRadius: 2))
}
}
UIColor.systemGreen.withAlphaComponent(0.4).setFill()
path.fill()
super.draw(rect)
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
private func setup() {
numberOfLines = 0
adjustsFontForContentSizeCategory = true
isUserInteractionEnabled = true
lineBreakMode = .byWordWrapping
contentMode = .redraw
clearsContextBeforeDrawing = true
isMultipleTouchEnabled = false
backgroundColor = .red.withAlphaComponent(0.1)
textContainer.lineFragmentPadding = 0
textContainer.maximumNumberOfLines = numberOfLines
textContainer.lineBreakMode = lineBreakMode
textContainer.layoutManager = layoutManager
layoutManager.textStorage = textStorage
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
}
override func layoutSubviews() {
super.layoutSubviews()
calculateRects()
}
private func calculateRects(){
linkRects.removeAll()
guard let attributedString = attributedText else {
return
}
textStorage.setAttributedString(attributedString)
let labelSize = frame.size
textContainer.size = labelSize
layoutManager.ensureLayout(for: textContainer)
let textBoundingBox = layoutManager.usedRect(for: textContainer)
print("labelSize: \(labelSize)")
print("textBoundingBox: \(textBoundingBox)")
var wholeLineRanges = [NSRange]()
layoutManager.enumerateLineFragments(forGlyphRange: NSRange(0 ..< layoutManager.numberOfGlyphs)) { _, rect, _, range, _ in
wholeLineRanges.append(range)
print("Whole line: \(rect), \(range)")
}
attributedString.enumerateAttribute(.link, in: NSRange(location: 0, length: attributedString.length)) { value, clickableRange, _ in
if value != nil {
var rectsForCurrentLink = [CGRect]()
wholeLineRanges.forEach { wholeLineRange in
if let linkPartIntersection = wholeLineRange.intersection(clickableRange) {
var rectForLinkPart = layoutManager.boundingRect(forGlyphRange: linkPartIntersection, in: textContainer)
rectForLinkPart.origin.y = rectForLinkPart.origin.y + (textContainer.size.height - textBoundingBox.height) / 2 // Adjust for vertical alignment
rectsForCurrentLink.append(rectForLinkPart)
print("Link rect: \(rectForLinkPart), \(linkPartIntersection)")
}
}
if !rectsForCurrentLink.isEmpty {
linkRects.append(rectsForCurrentLink)
}
}
}
print("linkRects: \(linkRects)")
setNeedsDisplay()
}
}
And I use this as such:
let label = MyLabel()
label.setContentHuggingPriority(.required, for: .vertical)
label.setContentHuggingPriority(.required, for: .horizontal)
view.addSubview(label)
label.snp.makeConstraints { make in
make.width.lessThanOrEqualTo(view.safeAreaLayoutGuide.snp.width).priority(.required)
make.horizontalEdges.greaterThanOrEqualTo(view.safeAreaLayoutGuide).priority(.required)
make.center.equalTo(view.safeAreaLayoutGuide).priority(.required)
}
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .justified
let s = NSMutableAttributedString(string: "Lorem Ipsum: ", attributes: [.font: UIFont.systemFont(ofSize: 17, weight: .regular), .paragraphStyle: paragraphStyle])
s.append(NSAttributedString(string: "This property controls the maximum number of lines to use in order to fit the label's text into its bounding rectangle.", attributes: [.link: URL(string: "https://news.ycombinator.com/") as Any, .foregroundColor: UIColor.link, .font: UIFont.systemFont(ofSize: 14, weight: .regular), .paragraphStyle: paragraphStyle]))
label.attributedText = s
Notice the paragraphStyle.alignment = .justified
This results in:
As you can see, the green rect background is starting a bit further to the right and also ending much further to the right.
If I set the alignment to be .left or .center, then it gives me the correct rects:
Also note that if I keep .justified but change the font size for the "Lorem Ipsom:" part to be a bit different, lets say 16 instead of 17, then it gives me the correct rect too:
Also note that if we remove some word from the string, then also it starts giving correct rect. It seems like if the first line is too squished, then it reports wrong rects.
Why is .justified text alignment giving me wrong rects? How can I fix it?