web-dev-qa-db-fra.com

Swift: touchez une partie du texte d'UILabel

J'ai un problème qui "boundingRectForGlyphRange" renvoie toujours CGRect.zero "0.0, 0.0, 0.0, 0.0". "boundingRectForGlyphRange" ne fonctionne pas. Par exemple, je code pour toucher une partie du texte de la fonction UILabel. La première partie de mon texte est "n’importe quel texte" et la seconde est "LIRE PLUS". Je veux que la reconnaissance de frappe ne fonctionne que lorsque je touche "LIRE PLUS". Si je touche sur un point quelconque de UILabel, "CGRectContainsPoint" renvoie toujours la valeur true, l'action

Voici mon 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")
        }
    }

C'est juste une démo pour tester ce que je veux faire:

 enter image description here

Toute aide serait grandement appréciée.

7
Ashley

Après avoir eu plusieurs problèmes avec ce genre de choses, en utilisant beaucoup de bibliothèques différentes, etc., j'ai trouvé une solution intéressante: http://samwize.com/2016/03/04/how- créer-plusieurs-liens-tappable-in-a-uilabel/

Il est sur le point d'étendre UITapGestureRegonizer et de détecter si la prise est dans la plage de la chaîne lorsqu'elle est déclenchée.

Voici la version mise à jour Swift 4 de cette 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)
    }

}

Pour simplifier la conversion de plage, vous avez également besoin de cette extension de plage.

extension Range where Bound == String.Index {
    var nsRange:NSRange {
        return NSRange(location: self.lowerBound.encodedOffset,
                   length: self.upperBound.encodedOffset -
                    self.lowerBound.encodedOffset)
    }
}

Une fois que vous avez cette extension, vous pouvez ajouter un geste de tapotement sur votre étiquette:

let tap = UITapGestureRecognizer(target: self, action: #selector(tapLabel(tap:)))
self.yourLabel.addGestureRecognizer(tap)
self.yourLabel.isUserInteractionEnabled = true

Voici la fonction pour gérer le robinet:

@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
    }
}
18
Beninho85

Swift 3. J'ai développé une 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
        } 
    }

Et maintenant je peux vérifier si le caractère tapé est dans la plage:

        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
        }
5
Naloiko Eugene

La pile de votre kit de texte est défectueuse. Vous avez oublié d'ajouter le conteneur de texte au gestionnaire de disposition! Par conséquent, il n'y a pas de texte à mettre en page et le gestionnaire de disposition ne peut signaler aucun rectangle de glyphe. Par conséquent, ce rectangle de glyphe est NSRectZero, raison pour laquelle vous ne pouvez jamais signaler de tapotement dans celui-ci.

Un autre problème est que vous appelez characterRangeForGlyphRange alors que vous devriez appeler glyphRangeForCharacterRange et que vous ne semblez pas savoir comment utiliser le résultat (en fait, vous le jetez).

Voici un code de travail qui montre uniquement la partie concernant l’utilisation de la pile de texte. Je commence par une chaîne "Bonjour à vous". Je vais montrer comment apprendre où le rect pour "to" est:

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)

Le résultat est {x 30.68 y 0 w 10.008 h 13.8}. Nous pouvons maintenant vérifier si un tap est dans cette droite. Allez-y et faites de même.

3
matt

Ceci est une réelle alternative facile pour quiconque est prêt à utiliser un textView. Je me rends compte que cette question concerne un UILabel, mais si vous lisez les commentaires sur certaines des réponses, elles ne fonctionnent pas pour certaines personnes et certaines d'entre elles sont très chargées en code, ce qui n'est pas très bon pour les débutants. Vous pouvez le faire en 11 étapes simples si vous êtes prêt à échanger un UILabel contre un UITextView.

Vous pouvez utiliser NSMutableAttributedString et une UITextView. UITextView a une méthode de délégué func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {. Une fois que vous avez défini la partie de la chaîne que vous souhaitez rendre tappable, la méthode déléguée l’activera.

Les 11 étapes sont énumérées ci-dessous dans les commentaires au-dessus de chaque morceau de 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
    let 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 used the value of 1 for the 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 // return false for this to work
        }

        return true
    }
}
3
Lance Samaria

Pour les étiquettes multilignes, vous devez définir la police textStorage ou la plage incorrecte sera renvoyée

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)

Il y a beaucoup de réponses à cette question. Cependant, de nombreuses personnes se plaignent de l'échec de la prise pour les étiquettes multilignes et que cela est correct pour la plupart des réponses sur cette page. La plage incorrecte pour le tap est renvoyée car la variable textStorage n'a pas la police correcte. 

let textStorage = NSTextStorage(attributedString: label.attributedText!)

Vous pouvez résoudre ce problème rapidement en ajoutant la police appropriée à votre instance textStorage:

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)

En réunissant tout cela, vous obtenez quelque chose comme ceci:

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
  }
}
1
DoesData