斯威夫特:点击UILabel文本的一部分

问题描述 投票:9回答:6

我有一个问题,“boundingRectForGlyphRange”总是返回CGRect.zero“0.0,0.0,0.0,0.0”。 “boundingRectForGlyphRange”无效。例如,我正在编写触摸UILabel功能的部分文本。我的文字的第一部分是“任何文字”,第二部分是“阅读更多”。我希望点击识别器仅在我触摸“READ MORE”时才能工作。如果我触摸UILabel上的任何一点,“CGRectContainsPoint”总是返回true,然后调用该动作

这是我的代码:

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

这只是一个测试我想做的事情的演示:

enter image description here

任何帮助将不胜感激。

ios swift uilabel nsattributedstring nslayoutmanager
6个回答
22
投票

在遇到这类问题的几个问题后,使用了很多不同的库等...我发现了一个有趣的解决方案:qazxsw poi

它将要扩展UITapGestureRegonizer并在触发时检测tap是否在字符串的范围内。

这是此扩展程序的更新Swift 4版本:

http://samwize.com/2016/03/04/how-to-create-multiple-tappable-links-in-a-uilabel/

要简化范围转换,您还需要此范围扩展

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

}

获得此扩展程序后,您可以为标签添加点按手势:

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

这是处理水龙头的功能:

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

6
投票

Swift 3.我开发了一个扩展:

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

现在我可以检查点击的字符是否在范围内:

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

6
投票

对于愿意使用textView的人来说,这是一个非常简单的选择。我意识到这个问题是关于一个UILabel,但是如果你阅读了一些答案的评论,它们对一些人不起作用,而且其中一些代码非常重,对初学者来说不是很好。如果您愿意将UILabel替换为UITextView,则可以通过11个简单步骤完成此操作。

你可以使用 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 } NSMutableAttributedString。 UITextView有一个委托方法UITextView。一旦设置了要进行tappable的字符串部分,委托方法就会激活它。

以下每个代码的注释中列出了11个步骤。

func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {

6
投票

swift 4.2

请在此处找到解决// 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 } } 特定文本action的解决方案。

Label

1)标签声明

enter image description here

2)将属性文本设置为标签

@IBOutlet weak var lblTerms: UILabel!

它看起来像上面的图像。

3)将tapLable动作方法添加到控制器

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

4)添加@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") } } 扩展

UITapGestureRecognizer

祝好运! :-)


3
投票

您的文本工具包堆栈有问题。您忘了将文本容器添加到布局管理器!因此,没有要布置的文本,并且布局管理器无法报告任何字形矩形。因此,字形rect是NSRectZero,这就是为什么你永远不能在其中报告点击的原因。

另一个问题是,当你应该调用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) } } 时,你正在调用characterRangeForGlyphRange,而你似乎不知道如何使用结果(事实上,你扔掉了结果)。

这是工作代码,仅显示有关使用文本堆栈的部分。我从字符串“Hello to you”开始。我将展示如何学习“to”的矩形:

glyphRangeForCharacterRange

结果是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) 。现在我们可以继续测试是否在那个矩形中。去做并做同样的事。


2
投票

对于多行标签,您必须设置textStorage字体,否则将返回不正确的范围

{x 30.68 y 0 w 10.008 h 13.8}

这个问题有很多答案。但是,有很多人抱怨多线标签的点击失败,这对于此页面上的大多数答案都是正确的。由于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) 没有正确的字体,因此返回了不正确的点击范围。

textStorage

您可以通过向let textStorage = NSTextStorage(attributedString: label.attributedText!) 实例添加正确的字体来快速解决此问题:

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)
© www.soinside.com 2019 - 2024. All rights reserved.