Skip to content

Instantly share code, notes, and snippets.

@leoiphonedev
Last active November 28, 2023 12:45
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save leoiphonedev/2d00d7de485b8f261e010fff333cba0f to your computer and use it in GitHub Desktop.
Save leoiphonedev/2d00d7de485b8f261e010fff333cba0f to your computer and use it in GitHub Desktop.
Extension for UITapGesture that contains a function to detect range of particular text in UILabel's text.
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)
}
}
@TendiF
Copy link

TendiF commented Nov 8, 2019

thanks this helping me

@tovkal
Copy link

tovkal commented Apr 21, 2020

Thank you! Super helpful

@canberkozcelik
Copy link

canberkozcelik commented May 13, 2020

might it be calculating indexOfCharacter wrongly?
I'm clicking the very end of the underlined text "User agreement":
textRange NSRange location=42, length=10
indexOfCharacter Int 56

@leoiphonedev
Copy link
Author

Yes its calculating wrong, please check the updated code here

might it be calculating indexOfCharacter wrongly?
I'm clicking the very end of the underlined text "User agreement":
textRange NSRange location=42, length=10
indexOfCharacter Int 56

@canberkozcelik
Copy link

so you have basically added 4 to indexOfCharacter? I don't think it's the right solution.

@leoiphonedev
Copy link
Author

leoiphonedev commented May 13, 2020 via email

@canberkozcelik
Copy link

It's reasonable 👍 just for the ones having multiline text with 4-5 text links in it, do not choose this approach

@applesakota
Copy link

Just delete (indexOfCharacter = indexOfCharacter + 4 ) this line of code :)

@Nikhil1827
Copy link

indexOfCharater is a let variable, how is it being changed in the next line?

@dhavaladhavdd
Copy link

As pointed out by @canberkozcelik this approach does not work for multi line text.

@dhavaladhavdd
Copy link

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

@geek-Shayan
Copy link

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)
}`

@Ganappriyan
Copy link

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment