I have a problem that boundingRectForGlyphRange
always returns CGRect.zero
"0.0, 0.0, 0.0, 0.0".
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") } } }
To make UILabel clickable you will need to enable user interaction for it. To enable user interaction for UILabel simply set the isUserInteractionEnabled property to true.
Adding a Tap Gesture Recognizer in Interface Builder You don't need to switch between the code editor and Interface Builder. Open Main. storyboard and drag a tap gesture recognizer from the Object Library and drop it onto the view we added earlier. The tap gesture recognizer appears in the Document Outline on the left.
#swift 4.2 Please find the solution here for getting specific text action
of Label
.
Label declaration
@IBOutlet weak var lblTerms: UILabel!
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.
Add the tapLabel
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") } }
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) } }
Make sure to do:
lblTerms.isUserInteractionEnabled = true
After having several issues with this kind of stuff, using a lot of different librairies, etc... I found an interesting solution: http://samwize.com/2016/03/04/how-to-create-multiple-tappable-links-in-a-uilabel/
It's about to extend UITapGestureRegonizer and detect if the tap is in the range of the string when triggered.
Here is the updated Swift 4 version of this 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 = 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) let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil) return NSLocationInRange(indexOfCharacter, targetRange) } }
To simplify range conversion, you also need this Range extension
extension Range where Bound == String.Index { var nsRange:NSRange { return NSRange(location: self.lowerBound.encodedOffset, length: self.upperBound.encodedOffset - self.lowerBound.encodedOffset) } }
Once you have this extension, you can add a tap gesture to your label:
let tap = UITapGestureRecognizer(target: self, action: #selector(tapLabel(tap:))) self.yourLabel.addGestureRecognizer(tap) self.yourLabel.isUserInteractionEnabled = true
Here is the function to handle the tap:
@objc func tapLabel(tap: UITapGestureRecognizer) { guard let range = self.yourLabel.text?.range(of: "Substring to detect")?.nsRange else { return } if tap.didTapAttributedTextInLabel(label: self.yourLabel, inRange: range) { // Substring tapped } }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With