extension UITapGestureRecognizer { | |
func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool { | |
// Create instances of NSLayoutManager, NSTextContainer and NSTextStorage | |
let layoutManager = NSLayoutManager() | |
let textContainer = NSTextContainer(size: CGSize.zero) | |
let textStorage = NSTextStorage(attributedString: label.attributedText!) | |
// Configure layoutManager and textStorage | |
layoutManager.addTextContainer(textContainer) | |
textStorage.addLayoutManager(layoutManager) | |
// Configure textContainer | |
textContainer.lineFragmentPadding = 0.0 | |
textContainer.lineBreakMode = label.lineBreakMode | |
textContainer.maximumNumberOfLines = label.numberOfLines | |
let labelSize = label.bounds.size | |
textContainer.size = labelSize | |
// Find the tapped character location and compare it to the specified range | |
let locationOfTouchInLabel = self.location(in: label) | |
let textBoundingBox = layoutManager.usedRect(for: textContainer) | |
let textContainerOffset = CGPoint(x: (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x, | |
y: (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y); | |
let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - textContainerOffset.x, | |
y: locationOfTouchInLabel.y - textContainerOffset.y); | |
var indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil) | |
indexOfCharacter = indexOfCharacter + 4 | |
return NSLocationInRange(indexOfCharacter, targetRange) | |
} | |
} |
After digging a bit I found out that multi line works only if the label's lineBreakMode is set to byWordWrapping
. This way the textBoundingBox's height is correctly calculated.
https://stackoverflow.com/questions/36043006/tap-on-a-part-of-text-of-uilabel
Still faced issue in the solution while tapping.
Check Here's the RIGHT SOLUTION.
`func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool {
// Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: CGSize.zero)
let textStorage = NSTextStorage(attributedString: label.attributedText!)
// Configure layoutManager and textStorage
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
// Configure textContainer
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = NSLineBreakMode.byWordWrapping
textContainer.maximumNumberOfLines = label.numberOfLines
let labelSize = label.bounds.size
textContainer.size = labelSize
// Find the tapped character location and compare it to the specified range
let locationOfTouchInLabel = self.location(in: label)
let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x , y: locationOfTouchInLabel.y)
let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
return NSLocationInRange(indexOfCharacter, targetRange)
}`
@geek-Shayan Thanks for the simplified code.
I am still facing some issue problem where the characterIndex is calculated incorrectly as the Number of lines increases.
I found UILabel's attributed text's default size is slightly larger than UITextView's Attributed Text (which uses layoutManager and textStorage inside it).
For now the working solution is giving the entire attributed string constant font size or scaledValue attributes: [.font: UIFont.systemFont(ofSize: UIFontMetrics.default.scaledValue(for: xSize))]
Tag me if anyone found proper solution
As pointed out by @canberkozcelik this approach does not work for multi line text.