I am creating a subclass of UILabel
in which I need to detect clicks in an NSAttributedString
. For various reasons, I am not allowed to use UITextView
. So, based on various other SO posts, I am using NSLayoutManager
, NSTextContainer
, and NSTextStorage
to calculate the textBoundingBox
.
All this works pretty well except my layoutManager.usedRect
gives me the wrong textBoundingBox
if my NSAttributedString
doesn't have an explicit font
attribute in it.
If I set a font attribute to it, then it gives me the correct textBoundingBox
.
Code:
import UIKit
class ClickableLabel3: UILabel, NSLayoutManagerDelegate {
private let layoutManager = NSLayoutManager()
private let textContainer = NSTextContainer(size: .zero)
private let textStorage = NSTextStorage()
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)
layoutManager.delegate = self
textStorage.addLayoutManager(layoutManager)
}
override func layoutSubviews() {
super.layoutSubviews()
calculateRects()
}
private func calculateRects(){
guard let attr = attributedText else {
return
}
let labelSize = frame.size
textContainer.size = labelSize
textStorage.setAttributedString(attr)
layoutManager.ensureLayout(for: textContainer)
let textBoundingBox = layoutManager.usedRect(for: textContainer)
print("labelSize: \(labelSize)")
print("textBoundingBox: \(textBoundingBox)")
}
}
Use the above as follows:
import SnapKit
import UIKit
let label = ClickableLabel3()
label.setContentHuggingPriority(.required, for: .vertical)
label.setContentHuggingPriority(.required, for: .horizontal)
view.addSubview(label)
label.snp.makeConstraints { make in
make.horizontalEdges.equalTo(view.safeAreaLayoutGuide)
make.verticalEdges.greaterThanOrEqualTo(view.safeAreaLayoutGuide).priority(.high)
make.centerY.equalToSuperview()
}
let attributedString = NSMutableAttributedString(string: "Lorem Ipsum is simply dummy text of the printing and typesetting industry:\n\n")
attributedString.append(NSAttributedString(string: "This is a link with long text... recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum", attributes: [.clickable: "https://news.ycombinator.com/", .foregroundColor: UIColor.link]))
label.attributedText = attributedString
Above prints incorrect textBoundingBox
:
labelSize: (440.0, 122.0)
textBoundingBox: (0.0, 0.0, 428.208984375, 55.2)
Notice the height
of the textBoundingBox
is much smaller. I believe this is due to it defaulting to some much smaller font size.
However, if I add my own font
attribute to the attributedString
, then it returns the correct textBoundingBox
:
let font = UIFont.systemFont(ofSize: 18, weight: .regular)
let attributedString = NSMutableAttributedString(string: "Lorem Ipsum is simply dummy text of the printing and typesetting industry:\n\n", attributes: [.font: font])
attributedString.append(NSAttributedString(string: "This is a link with long text... recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum", attributes: [.clickable: "https://news.ycombinator.com/", .foregroundColor: UIColor.link, .font: font]))
label.attributedText = attributedString
This prints correct textBoundingBox
:
labelSize: (440.0, 129.0)
textBoundingBox: (0.0, 0.0, 433.6853386759758, 128.8828125)
Is there a way for me to handle the lack of font attribute in the attributed string without explicitly adding it?
I tried looking if NSLayoutManager
or NSTextStorage
or NSTextContainer
had a font property I could set but can't find one. How can I override the default font?
I figured out the answer.
Add below AFTER line textStorage.setAttributedString(attr)
:
textStorage.enumerateAttribute(.font, in: NSRange(location: 0, length: textStorage.length), options: []) { value, subrange, pointeeStop in
guard value == nil else { return }
textStorage.addAttribute(.font, value: font as Any, range: subrange)
}
In this case, it enumerates the font
attribute and if no font exists, then it applies the UILabel
's current font.