Swift : tap on a part of text of UILabel

Ashley picture Ashley · Mar 16, 2016 · Viewed 38.1k times · Source

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 touching on a part of text of UILabel feature. My text has first part is "any text" and second one is "READ MORE". I want the tap recognizer only work when I touch "READ MORE". If I touch on any point on UILabel, "CGRectContainsPoint" always return true,then the action called

Here my code:

override func viewDidLoad() {
        super.viewDidLoad()

        // The full string

        let firstPart:NSMutableAttributedString = NSMutableAttributedString(string: "Lorem ipsum dolor set amit ", attributes: [NSFontAttributeName: UIFont.systemFontOfSize(13)])
        firstPart.addAttribute(NSForegroundColorAttributeName, value: UIColor.blackColor(),
            range: NSRange(location: 0, length: firstPart.length))
        info.appendAttributedString(firstPart)

        // The "Read More" string that should be touchable
        let secondPart:NSMutableAttributedString = NSMutableAttributedString(string: "READ MORE", attributes: [NSFontAttributeName: UIFont.systemFontOfSize(14)])
        secondPart.addAttribute(NSForegroundColorAttributeName, value: UIColor.blackColor(),
            range: NSRange(location: 0, length: secondPart.length))
        info.appendAttributedString(secondPart)

        lblTest.attributedText = info

        // Store range of chars we want to detect touches for
        moreStringRange = NSMakeRange(firstPart.length, secondPart.length)
        print("moreStringRange\(moreStringRange)")

        tapRec.addTarget(self, action: "didTap:")
        lblTest.addGestureRecognizer(tapRec)

    }


    func didTap(sender:AnyObject) {
        // Storage class stores the string, obviously
        let textStorage:NSTextStorage = NSTextStorage(attributedString: info)
        // The storage class owns a layout manager
        let layoutManager:NSLayoutManager = NSLayoutManager()
        textStorage.addLayoutManager(layoutManager)

        // Layout manager owns a container which basically
        // defines the bounds the text should be contained in
        let textContainer:NSTextContainer = NSTextContainer(size: lblTest.frame.size)
        textContainer.lineFragmentPadding = 0
        textContainer.lineBreakMode = lblTest.lineBreakMode

        // Begin computation of actual frame
        // Glyph is the final display representation
        var glyphRange = NSRange()
        // Extract the glyph range
        layoutManager.characterRangeForGlyphRange(moreStringRange!, actualGlyphRange: &glyphRange)

        // Compute the rect of glyph in the text container
        print("glyphRange\(glyphRange)")
        print("textContainer\(textContainer)")
        let glyphRect:CGRect = layoutManager.boundingRectForGlyphRange(glyphRange, inTextContainer: textContainer)

        // Final rect relative to the textLabel.
        print("\(glyphRect)")

        // Now figure out if the touch point is inside our rect
        let touchPoint:CGPoint = tapRec.locationOfTouch(0, inView: lblTest)

        if CGRectContainsPoint(glyphRect, touchPoint) {
            print("User tapped on Read More. So show something more")
        }
    }

It's just a demo to test for what I want to do:

enter image description here

Any help would be greatly appreciated.

Answer

Ilesh P picture Ilesh P · Mar 19, 2019

swift 4.2

Please find the solution here for getting specific text action of Label.

enter image description here

1) Label declaration

@IBOutlet weak var lblTerms: UILabel!

2) Set attributed text to the label

let text = "Please agree for Terms & Conditions."
lblTerms.text = text
self.lblTerms.textColor =  UIColor.white
let underlineAttriString = NSMutableAttributedString(string: text)
let range1 = (text as NSString).range(of: "Terms & Conditions.")
        underlineAttriString.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: range1)
        underlineAttriString.addAttribute(NSAttributedString.Key.font, value: UIFont.init(name: Theme.Font.Regular, size: Theme.Font.size.lblSize)!, range: range1)
        underlineAttriString.addAttribute(NSAttributedString.Key.foregroundColor, value: Theme.color.primaryGreen, range: range1)
lblTerms.attributedText = underlineAttriString
lblTerms.isUserInteractionEnabled = true
lblTerms.addGestureRecognizer(UITapGestureRecognizer(target:self, action: #selector(tapLabel(gesture:))))

It looks like the above image.

3) Add the tapLable action method to the controller

@IBAction func tapLabel(gesture: UITapGestureRecognizer) {
   let termsRange = (text as NSString).range(of: "Terms & Conditions")
   // comment for now
   //let privacyRange = (text as NSString).range(of: "Privacy Policy")

   if gesture.didTapAttributedTextInLabel(label: lblTerms, inRange: termsRange) {
       print("Tapped terms")
   } else if gesture.didTapAttributedTextInLabel(label: lblTerms, inRange: privacyRange) {
       print("Tapped privacy") 
   } else {                
       print("Tapped none")
   }
}

4) Add UITapGestureRecognizer extension

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 = CGPointMake((labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
                                              //(labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y);
        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 = CGPointMake(locationOfTouchInLabel.x - textContainerOffset.x,
                                                        // locationOfTouchInLabel.y - textContainerOffset.y);
        let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - textContainerOffset.x, y: locationOfTouchInLabel.y - textContainerOffset.y)
        let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
        return NSLocationInRange(indexOfCharacter, targetRange)
    }

}

Good luck! :-)