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

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

https://i.gyazo.com/a612d5f1d36089d5387083ec5bc20214.png

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

9 ответов

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

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

3) Добавьте метод действия 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")
   }
}

4) Добавить UITapGestureRecognizer расширение

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

}

Удачи!:-)

После нескольких проблем с такими вещами, использования множества разных библиотек и т. Д. Я нашел интересное решение: http://samwize.com/2016/03/04/how-to-create-multiple-tappable-links-in-a-uilabel/

Он собирается расширить 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
    }
}

Чтобы включить многострочное нажатие и не хотите создавать подкласс 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))

       // 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 example@yourdomain.com"
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: "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")
    }
}

Ты можешь использовать 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
    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(NSAttributedStringKey.font, value: UIFont.systemFont(ofSize: 17), range: NSMakeRange(0, tappableText.length))
        tappableText.addAttribute(NSAttributedStringKey.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(NSAttributedStringKey.underlineStyle, value: 1, range: NSMakeRange(0, tappableText.length))
        tappableText.addAttribute(NSAttributedStringKey.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 NSAttributedStringKey.link and the value "makeMeTappable" to link the NSAttributedStringKey.link to the method. FYI "makeMeTappable" is a name I choose for clarity, you can use anything like "anythingYouCanThinkOf"
        tappableText.addAttribute(NSAttributedStringKey.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
    }
}

Для многострочных меток вы должны установить шрифт 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
  }
}

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
        }

Ваш стек текстового набора неисправен. Вы забыли добавить текстовый контейнер в менеджер макетов! Поэтому нет текста для разметки, и менеджер макета не может сообщить о любом прямоугольнике глифа. Следовательно, этим глифом является 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}, Теперь мы можем проверить, находится ли ответвление в этом прямоугольнике. Иди и делай так же.

Согласитесь с ответом, получившим наибольшее количество голосов. Но этот ответ подходит для центрального 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: linkedword", часть "magina" является обычным текстом, "linkedword" является ссылкой. Когда вы устанавливаете текст для UILabel, вам нужно подключить текст и один пробел, в противном случае, когда вы нажмете на конец метки, функция 'didTapAttributedTextInLabel' вернет значение true, но на самом деле оно выходит за пределы текста.

      var attrString = NSMutableAttributedString(string: text + " ")//add one space to present 

Я разработал небольшую библиотеку SmartString для iOS , которую можно использовать для обработки касаний определенной подстроки внутри UILabel.

Использование:

      // Example 1
let smartString = "hello, " + "tap here".onTap { string in 
    print(string) // -> "tap here"
}
label.smartString = smartString

// Example 2 (style your string to make it looks nice)
let smartString = "hello" 
+ "tap here".font(.systemFont(ofSize: 22))
    .color(.blue)
    .underline()
    .onTap { string in 
        print(string) // -> "tap here"
}
label.smartString = smartString
Другие вопросы по тегам