Swift : tap on a part of text of UILabel

前端 未结 7 878
北海茫月
北海茫月 2020-12-02 21:36

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

相关标签:
7条回答
  • 2020-12-02 22:02

    To enable multiline tappable & don't want to subclass the UILabel then:

    • Write Extension function for UITapGestureRecognizer
    extension UITapGestureRecognizer {
    
       func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool {
           guard let attributedText = label.attributedText else { return false }
    
           let mutableStr = NSMutableAttributedString.init(attributedString: attributedText)
           mutableStr.addAttributes([NSAttributedString.Key.font : label.font!], range: NSRange.init(location: 0, length: attributedText.length))
    
           // Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
           let layoutManager = NSLayoutManager()
           let textContainer = NSTextContainer(size: CGSize.zero)
           let textStorage = NSTextStorage(attributedString: mutableStr)
    
           // 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)
       }
    
    }
    
    • Configure your UILable
    label.text = "For any type of query please call us on +9186XXX-XXXXX or mail us at example@yourdomain.com"
    label.isUserInteractionEnabled = true
    label.lineBreakMode = .byWordWrapping
    let tapGesture = UITapGestureRecognizer.init(target: self, action: #selector(tappedOnLabel(_:)))
    tapGesture.numberOfTouchesRequired = 1
    label.addGestureRecognizer(tapGesture)
    
    • Add the gesture recogniser selector function:
    @objc func tappedOnLabel(_ gesture: UITapGestureRecognizer) {
        guard let text = label.text else { return }
        let numberRange = (text as NSString).range(of: "+9186XXX-XXXXX")
        let emailRange = (text as NSString).range(of: "example@yourdomain.com")    
        if gesture.didTapAttributedTextInLabel(label: self.label, inRange: numberRange) {
            print("number tapped")
        } else if gesture.didTapAttributedTextInLabel(label: self.label, inRange: emailRange) {
            print("Email tapped")
        }
    }
    
    0 讨论(0)
  • 2020-12-02 22:03

    swift 4.2

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

    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! :-)

    0 讨论(0)
  • 2020-12-02 22:06

    This is a real easy alternative for anyone who is willing to use a textView. I realize this question is about a UILabel but if you read the comments on some of the answers they don't work for some people and some of them are very code heavy which isn't very good for beginners. You can do this in 11 simple steps if your willing to swap out a UILabel for a UITextView.

    You can use NSMutableAttributedString and a UITextView. The UITextView has a delegate method: func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool { ... }. Once you set the part of the string that you want to make tappable the delegate method will activate it.

    The 11 steps are listed below in the comments above each piece of code.

    // 1st **BE SURE TO INCLUDE** UITextViewDelegate to the view controller's class
    class VewController: UIViewController, UITextViewDelegate {
    
        // 2nd use a programmatic textView or use the textView from your storyboard
        lazy var yourTextView: UITextView = {
            let textView = UITextView()
            textView.textAlignment = .center
            textView.isEditable = false
            textView.showsVerticalScrollIndicator = false
            return textView
        }()
    
       override func viewDidLoad() {
            super.viewDidLoad()
    
            // 3rd in viewDidLoad set the textView's delegate
            yourTextView.delegate = self
    
            // 4th create the first piece of the string you don't want to be tappable
            let regularText = NSMutableAttributedString(string: "any text ", attributes: [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 17), NSAttributedStringKey.foregroundColor: UIColor.black])
    
            // 5th create the second part of the string that you do want to be tappable. I used a blue color just so it can stand out.
            let tappableText = NSMutableAttributedString(string: "READ MORE")
            tappableText.addAttribute(NSAttributedString.Key.font, value: UIFont.systemFont(ofSize: 17), range: NSMakeRange(0, tappableText.length))
            tappableText.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.blue, range: NSMakeRange(0, tappableText.length))
    
            // 6th this ISN'T NECESSARY but this is how you add an underline to the tappable part. I also used a blue color so it can match the tappableText and set the value to 1 for the line height. The length of the underline is based on the tappableText's length using NSMakeRange(0, tappableText.length)
            tappableText.addAttribute(NSAttributedString.Key.underlineStyle, value: 1, range: NSMakeRange(0, tappableText.length))
            tappableText.addAttribute(NSAttributedString.Key.underlineColor, value: UIColor.blue, range: NSMakeRange(0, tappableText.length))
    
            // 7th this is the important part that connects the tappable link to the delegate method in step 11
            // use NSAttributedString.Key.link and the value "makeMeTappable" to link the NSAttributedString.Key.link to the method. FYI "makeMeTappable" is a name I choose for clarity, you can use anything like "anythingYouCanThinkOf"
            tappableText.addAttribute(NSAttributedString.Key.link, value: "makeMeTappable", range: NSMakeRange(0, tappableText.length))
    
            // 8th *** important append the tappableText to the regularText ***
            regularText.append(tappableText)
    
            // 9th set the regularText to the textView's attributedText property
            yourTextView.attributedText = regularText 
       }
    
       // 10th add the textView's delegate method that activates urls. Make sure to return false for the tappable part
       func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
        
            // 11th use the value from the 7th step to trigger the url inside this method
            if URL.absoluteString == "makeMeTappable" {
    
                // in this situation I'm using the tappableText to present a view controller but it can be used for whatever you trying to do
                let someVC = SomeController()
                let navVC = UINavigationController(rootViewController: someVC)
                present(navVC, animated: true, completion: nil)
    
                return false // *** IMPORTANT return false for this to actually work ***
            }
    
            return true
        }
    }
    
    0 讨论(0)
  • 2020-12-02 22:08

    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<Index>] {
        var ranges: [Range<Index>] = []
        while let range = self.range(of: substring, options: options, range: (ranges.last?.upperBound ?? self.startIndex) ..< self.endIndex, locale: locale) {
          ranges.append(range)
        }
        return ranges
      }
    }
    
    0 讨论(0)
  • 2020-12-02 22:16

    Your text kit stack is faulty. You forgot to add the text container to the layout manager! Therefore there is no text to lay out, and the layout manager cannot report any glyph rect. Therefore that glyph rect is NSRectZero, which is why you can never report a tap within it.

    Another problem is that you are calling characterRangeForGlyphRange when you should be calling glyphRangeForCharacterRange, and you don't seem to know how to use the result (in fact, you throw away the result).

    Here is working code that shows just the part about using the text stack. I start with a string "Hello to you". I will show how to learn where the rect for "to" is:

    let s = "Hello to you"
    let ts = NSTextStorage(
        attributedString: NSAttributedString(string:s))
    let lm = NSLayoutManager()
    ts.addLayoutManager(lm)
    let tc = NSTextContainer(size: CGSizeMake(4000,400))
    lm.addTextContainer(tc) // ****
    tc.lineFragmentPadding = 0
    let toRange = (s as NSString).rangeOfString("to")
    let gr = lm.glyphRangeForCharacterRange(
        toRange, actualCharacterRange: nil) // ****
    let glyphRect = lm.boundingRectForGlyphRange(
        gr, inTextContainer: tc)
    

    The result is {x 30.68 y 0 w 10.008 h 13.8}. Now we can proceed to test whether a tap is in that rect. Go Ye And Do Likewise.

    0 讨论(0)
  • 2020-12-02 22:22

    Swift 3. I've developed an extension:

     extension UILabel {
            ///Find the index of character (in the attributedText) at point
            func indexOfAttributedTextCharacterAtPoint(point: CGPoint) -> Int {
                assert(self.attributedText != nil, "This method is developed for attributed string")
                let textStorage = NSTextStorage(attributedString: self.attributedText!)
                let layoutManager = NSLayoutManager()
                textStorage.addLayoutManager(layoutManager)
                let textContainer = NSTextContainer(size: self.frame.size)
                textContainer.lineFragmentPadding = 0
                textContainer.maximumNumberOfLines = self.numberOfLines
                textContainer.lineBreakMode = self.lineBreakMode
                layoutManager.addTextContainer(textContainer)
    
                let index = layoutManager.characterIndex(for: point, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
                return index
            } 
        }
    

    And now I can check if the tapped character is in range:

            let range = SOME_RANGE
            let tapLocation = gesture.location(in: MY_TEXT_LABEL)
            let index = textLbl.indexOfAttributedTextCharacterAtPoint(point: tapLocation)
    
            if index > range.location && index < range.location + range.length {
             //YES, THE TAPPED CHARACTER IS IN RANGE
            }
    
    0 讨论(0)
提交回复
热议问题