Swift: нажмите на часть текста UILabel

У меня проблема, что "boundingRectForGlyphRange" всегда возвращает CGRect.zero "0.0, 0.0, 0.0, 0.0". «boundingRectForGlyphRange» не работает. Например, я кодирую касание части текста функции UILabel. В моем тексте первая часть — «любой текст», а вторая — «ПОДРОБНЕЕ». Я хочу, чтобы распознаватель касаний работал только тогда, когда я нажимаю «ПОДРОБНЕЕ». Если я коснусь любой точки на 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")
        }
    }

Это просто демо для проверки того, что я хочу сделать:

введите описание изображения  здесь

Любая помощь будет принята с благодарностью.


person Ashley    schedule 16.03.2016    source источник
comment
Надеюсь, это поможет вам узнать больше   -  person Muzahid    schedule 16.03.2016
comment
Как насчет использования двух меток?   -  person koen    schedule 16.03.2016
comment
@Koen: Это всего лишь демонстрация. В моем тексте много частей с вложениями. Я не могу использовать много ярлыков.   -  person Ashley    schedule 16.03.2016
comment
@Md.MuzahidulIslam: Как я уже сказал, это всего лишь демонстрация, подробнее (может быть, что-то еще) может не быть в конце строки.   -  person Ashley    schedule 16.03.2016
comment
Затем проверьте один stackoverflow.com/questions/32175144/   -  person Muzahid    schedule 16.03.2016
comment
@Md.MuzahidulIslam. Пожалуйста, проверьте изображение. изображение, чтобы понять.   -  person Ashley    schedule 16.03.2016
comment
Другой возможный способ сделать это: stackoverflow.com/questions/20541676/   -  person koen    schedule 16.03.2016


Ответы (8)


#swift 4.2 Здесь вы найдете решение для получения конкретного текста action из Label.

введите здесь описание изображения

  1. Объявление ярлыка

    @IBOutlet weak var lblTerms: UILabel!
    
  2. Установить атрибутированный текст для метки

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

Это похоже на изображение выше.

  1. Добавьте в контроллер метод действия tapLable.

    @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")
    }
    }
    
  2. Добавить расширение UITapGestureRecognizer

    расширение 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)
     }
    
    }
    

Обязательно выполните:

lblTerms.isUserInteractionEnabled = true

Удачи! :-)

person Ilesh P    schedule 19.03.2019
comment
@llesh ты хоть представляешь себе несколько подтекстов? Например, у меня есть такой текст: Привет, мир! Привет. Есть ли способ обнаружить нажатие на 2-е слово привет? - person R4j; 25.06.2019
comment
также следует добавить выравнивание (label.textAlignment) к атрибутированной строке для правильной работы (т.е. центрированный текст), как в stackoverflow.com/a/40946288/ 1619680 - person Andrew; 18.04.2020
comment
По какой-то причине для меня нажатие работает, но оно отличается значительным количеством пикселей (например, около 10 или 20 в моем случае). Любые идеи, почему это может быть? - person Joseph Astrahan; 07.05.2020
comment
У меня есть многострочный текст со специальными символами Юникода. - person Joseph Astrahan; 07.05.2020
comment
вот данные для моих переменных gesuture: LabelSize= (315.0, 43.0) LocationOfTouchInLabel= (235.0, 25.5) TextBoundingBox= (64.8134765625, 0.0, 185.373046875, 13.8) LocationOfTouchInTextContainer= (235.0, 10.9) IndexOf32Range= } - person Joseph Astrahan; 07.05.2020
comment
@JosephAstrahan Я столкнулся с аналогичной проблемой на нескольких языках. Вы нашли какое-либо решение для этого? - person Chintan Shah; 24.11.2020
comment
@chintan Shah К сожалению, у меня нет ... пожалуйста, поделитесь, если найдете решение. Я нашел некоторые обходные пути... но я не уверен, что ими стоит делиться. - person Joseph Astrahan; 11.12.2020

После нескольких проблем с подобными вещами, использования множества разных библиотек и т. д. я нашел интересное решение: http://samwize.com/2016/03/04/how-to-создать-несколько-переключаемых-ссылок-в-уилабеле/

Он собирается расширить UITapGestureRegonizer и определить, находится ли касание в диапазоне строки при срабатывании.

Вот обновленная версия Swift 4 этого расширения:

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

Вот функция для обработки тапа:

@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
    }
}
person Beninho85    schedule 25.10.2017
comment
то же самое не работает для меня также. Я думаю, что ответ только для одной метки строки, а не для многострочной метки. - person Bijender Singh Shekhawat; 18.07.2018
comment
Для многострочного текста установите для режима разрыва строки метки значение Обрезать хвост. - person Basheer; 26.07.2018
comment
Не удается прикоснуться к тексту пользовательской метки UITableViewCell - person IKKA; 02.01.2019
comment
попробуйте это self.yourLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapLabel(tap:)))), это сработает. - person davut dev; 22.04.2019
comment
Это работает как шарм! Спасибо. for range in ranges { nsRanges.append(range.nsRange(in: subString)) } Найдите его здесь: stackoverflow.com/questions /43233070/ - person mig_loren; 24.07.2021

Это действительно простая альтернатива для тех, кто хочет использовать textView. Я понимаю, что этот вопрос касается UILabel, но если вы прочитаете комментарии к некоторым ответам, они не работают для некоторых людей, а некоторые из них очень тяжелые для кода, что не очень хорошо для начинающих. Вы можете сделать это за 11 простых шагов, если хотите заменить UILabel на UITextView.

Вы можете использовать NSMutableAttributedString и UITextView. UITextView имеет метод делегата: func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool { ... }. Как только вы установите часть строки, которую вы хотите сделать доступной, метод делегата активирует ее.

11 шагов перечислены ниже в комментариях над каждым фрагментом кода.

// 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
    }
}
person Lance Samaria    schedule 10.09.2018
comment
Переопределить shouldInteractWith — чертовски красивое решение, спасибо за это. Почти любое другое решение включает в себя определение физического местоположения касания фрагмента текста. - person Amin; 08.07.2020
comment
@FedericoZanetello np, рад, что смог помочь ???? ура!!! ???? ???? ???? - person Lance Samaria; 24.11.2020
comment
Хороший ответ. Спасибо @LanceSamaria - person KSR; 20.02.2021
comment
@Амин прав! Спасибо за этот отличный ответ. - person אורי orihpt; 25.07.2021

Для многострочных меток необходимо установить шрифт textStorage, иначе будет возвращен неправильный диапазон

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)

Собрав все вместе, вы получите что-то вроде этого:

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
  }
}
person DoesData    schedule 04.02.2019
comment
Ты спас мой день, чувак! Единственный правильный ответ с очень важным добавлением шрифта. Благодарить) - person sacred; 19.02.2020
comment
Не знаю, почему установка шрифта метки на attributeString исправляет это, но это так. Спасибо за журнал @DoesData! Спас мой день - person Pierluigi Cifani; 26.03.2020
comment
Да.. действительно здорово!! Я трачу около 3 часов .. наконец, это решение мне очень помогло! - person Ramesh Kumar; 14.05.2020
comment
Это должен быть принятый ответ. Спасибо, я застрял на этом в течение нескольких дней! - person Danny Buonocore; 01.04.2021

Чтобы включить возможность многострочного нажатия и не создавать подкласс UILabel, выполните следующие действия:

  • Функция расширения записи для 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))
       
       // If the label have text alignment. Delete this code if label have a default (left) aligment. Possible to add the attribute in previous adding.
       let paragraphStyle = NSMutableParagraphStyle()
       paragraphStyle.alignment = .center
       mutableStr.addAttributes([NSAttributedString.Key.paragraphStyle : paragraphStyle], range: NSRange(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)
   }
   
}
  • Настройте свой UILable
label.text = "For any type of query please call us on +9186XXX-XXXXX or mail us at [email protected]"
label.isUserInteractionEnabled = true
label.lineBreakMode = .byWordWrapping
let tapGesture = UITapGestureRecognizer.init(target: self, action: #selector(tappedOnLabel(_:)))
tapGesture.numberOfTouchesRequired = 1
label.addGestureRecognizer(tapGesture)
  • Добавьте функцию выбора распознавателя жестов:
@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: "[email protected]")    
    if gesture.didTapAttributedTextInLabel(label: self.label, inRange: numberRange) {
        print("number tapped")
    } else if gesture.didTapAttributedTextInLabel(label: self.label, inRange: emailRange) {
        print("Email tapped")
    }
}
person Rahul    schedule 17.05.2020
comment
Спас мой день Спасибо - person Dilip Tiwari; 24.02.2021

Swift 3. Я разработал расширение:

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

И теперь я могу проверить, находится ли выбранный персонаж в пределах досягаемости:

        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
        }
person Naloiko Eugene    schedule 24.02.2017
comment
Спасибо. Это действительно полезно. В многострочном тексте нижняя строка не нажимается, когда мы реализуем это расширение. Кроме того, мы пытаемся let textContainer = NSTextContainer(size: CGSize(width: (self.label?.frame.width)!, height: (self.label.frame.height)+100)), но без шансов. - person Photon Point; 09.03.2017
comment
Хороший подход. Но это не касается текста, не выровненного по левому краю (например, по центру или справа). Похоже, нет способа передать выравнивание от UILabel к NSTextContainer - person brigadir; 25.09.2017
comment
Увеличение высоты UILabel на 5 пикселей позволяет мне обнаруживать текст в последней строке. - person Rajasekhar Pasupuleti; 05.03.2020

Ваш стек текстового набора неисправен. Вы забыли добавить текстовый контейнер в менеджер компоновки! Поэтому нет текста для компоновки, и менеджер компоновки не может сообщить ни о каком прямоугольнике глифа. Следовательно, этот прямоугольник с глифом — это NSRectZero, поэтому вы никогда не сможете сообщить об касании внутри него.

Другая проблема заключается в том, что вы вызываете characterRangeForGlyphRange вместо того, чтобы вызывать glyphRangeForCharacterRange, и вы, кажется, не знаете, как использовать результат (фактически, вы отбрасываете результат).

Вот рабочий код, который показывает только часть об использовании текстового стека. Я начинаю со строки «Привет тебе». Я покажу, как узнать, где находится прямоугольник для «to»:

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)

Результат {x 30.68 y 0 w 10.008 h 13.8}. Теперь мы можем приступить к проверке, есть ли кран в этом прямоугольнике. Иди и поступай так же.

person matt    schedule 16.03.2016
comment
где этикетка? - person Bijender Singh Shekhawat; 18.07.2018

Согласитесь с ответом, получившим наибольшее количество голосов. Но этот ответ подходит для центрального textAlignment, когда textAlignment вашего UIlabel не является центральным, а также ваш UILabel имеет edgeInset, вам нужно изменить некоторый код расчета. Мой ответ на сообщение рассматривает textAlignment и edgetInset.

func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange, edgeInset: UIEdgeInsets? = nil) -> 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)
        let alignment = label.textAlignment ?? .center
        // Configure textContainer
        textContainer.lineFragmentPadding = 0.0
        textContainer.lineBreakMode = label.lineBreakMode
        textContainer.maximumNumberOfLines = label.numberOfLines
        let labelSize = CGSize(width: label.bounds.width - (edgeInset?.left ?? 0) - (edgeInset?.right ?? 0), height: label.bounds.height - (edgeInset?.top
                ?? 0) - (edgeInset?.bottom ?? 0))
        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)
        var xOffset: CGFloat = 0
        var yOffset: CGFloat = 0
        if alignment == .left {
            xOffset = (edgeInset?.left ?? 0) - textBoundingBox.origin.x
            yOffset = (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y + (edgeInset?.top ?? 0)
        } else if alignment == .right {
            xOffset = label.bounds.width - (edgeInset?.right ?? 0) - labelSize.width - textBoundingBox.origin.x
            yOffset = (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y + (edgeInset?.top ?? 0)
        } else {
            xOffset = (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x + (edgeInset?.left ?? 0)
            yOffset = (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y + (edgeInset?.top ?? 0)
        }
        let textContainerOffset = CGPoint(
            x: xOffset,
            y: yOffset
        )
        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)
    }

И еще одна ситуация должна быть рассмотрена. Если ваш текст magina: связанное слово, часть magina - это обычный текст, связанное слово - ссылка. Когда вы устанавливаете текст для UILabel, вам нужно соединить текст и один пробел, в противном случае, когда вы нажмете конец метки, функция 'didTapAttributedTextInLabel' вернет true, но на самом деле она выходит за пределы текста.

var attrString = NSMutableAttributedString(string: text + " ")//add one space to present 
person dong liu    schedule 17.07.2021