I have a problem that \"boundingRectForGlyphRange\" always returns CGRect.zero \"0.0, 0.0, 0.0, 0.0\". \"boundingRectForGlyphRange\" is not working. For example, I am coding
For multi-line labels you have to set the textStorage font or the incorrect range will be returned
guard let attributedString = self.attributedText else { return }
let mutableAttribString = NSMutableAttributedString(attributedString: attributedString)
mutableAttribString.addAttributes([NSAttributedString.Key.font: myFont], range: NSRange(location: 0, length: attributedString.length))
let textStorage = NSTextStorage(attributedString: mutableAttribString)
There are a lot of answers to this question. However, there are many people complaining that the tap fails for multi-line labels and that is correct for most answers on this page. The incorrect range for the tap is returned because the textStorage
doesn't have the correct font.
let textStorage = NSTextStorage(attributedString: label.attributedText!)
You can fix this quickly by adding the correct font to your textStorage
instance:
guard let attributedString = self.attributedText else { return -1 }
let mutableAttribString = NSMutableAttributedString(attributedString: attributedString)
mutableAttribString.addAttributes([NSAttributedString.Key.font: myFont], range: NSRange(location: 0, length: attributedString.length))
let textStorage = NSTextStorage(attributedString: mutableAttribString)
Putting it all together you get something like this:
protocol AtMentionsLabelTapDelegate: class {
func labelWasTappedForUsername(_ username: String)
}
class AtMentionsLabel: UILabel {
private var tapGesture: UITapGestureRecognizer = UITapGestureRecognizer()
weak var tapDelegate: AtMentionsLabelTapDelegate?
var mentions: [String] = [] // usernames to style
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() {
isUserInteractionEnabled = true
lineBreakMode = .byWordWrapping
tapGesture = UITapGestureRecognizer()
tapGesture.addTarget(self, action: #selector(handleLabelTap(recognizer:)))
tapGesture.numberOfTapsRequired = 1
tapGesture.isEnabled = true
addGestureRecognizer(tapGesture)
}
@objc func handleLabelTap(recognizer: UITapGestureRecognizer) {
let tapLocation = recognizer.location(in: self)
let tapIndex = indexOfAttributedTextCharacterAtPoint(point: tapLocation)
for username in mentions {
if let ranges = self.attributedText?.rangesOf(subString: username) {
for range in ranges {
if tapIndex > range.location && tapIndex < range.location + range.length {
tapDelegate?.labelWasTappedForUsername(username)
return
}
}
}
}
}
func indexOfAttributedTextCharacterAtPoint(point: CGPoint) -> Int {
guard let attributedString = self.attributedText else { return -1 }
let mutableAttribString = NSMutableAttributedString(attributedString: attributedString)
// Add font so the correct range is returned for multi-line labels
mutableAttribString.addAttributes([NSAttributedString.Key.font: font], range: NSRange(location: 0, length: attributedString.length))
let textStorage = NSTextStorage(attributedString: mutableAttribString)
let layoutManager = NSLayoutManager()
textStorage.addLayoutManager(layoutManager)
let textContainer = NSTextContainer(size: frame.size)
textContainer.lineFragmentPadding = 0
textContainer.maximumNumberOfLines = numberOfLines
textContainer.lineBreakMode = lineBreakMode
layoutManager.addTextContainer(textContainer)
let index = layoutManager.characterIndex(for: point, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
return index
}
}
extension NSAttributedString {
func rangesOf(subString: String) -> [NSRange] {
var nsRanges: [NSRange] = []
let ranges = string.ranges(of: subString, options: .caseInsensitive, locale: nil)
for range in ranges {
nsRanges.append(range.nsRange)
}
return nsRanges
}
}
extension String {
func ranges(of substring: String, options: CompareOptions = [], locale: Locale? = nil) -> [Range] {
var ranges: [Range] = []
while let range = self.range(of: substring, options: options, range: (ranges.last?.upperBound ?? self.startIndex) ..< self.endIndex, locale: locale) {
ranges.append(range)
}
return ranges
}
}