It’s always funny when you google “how do I…” and Google shows you a result that you actually wrote.
I wanted to solve this problem, and it looks like I did it years ago in Objective-C.
So, I re-wrote it in Swift that you can basically just copy-paste-use. You’re welcome.
// // TappableLabel.swift // // Created by Stephen O'Connor on 07.10.20. // MIT License. You will send no lawyers here. Have fun. // taken from here: https://horseshoe7.wordpress.com/2015/12/10/detect-touches-on-attributed-text-in-uilabel/ import UIKit typealias LabelLink = (text: String, link: Any?) protocol TappableLabelDelegate: class { func didTapOnLink(_ link: LabelLink, in tappableLabel: TappableLabel) } extension NSAttributedString.Key { static let custom = NSAttributedString.Key("CustomAttribute") } class TappableLabel: UILabel { weak var delegate: TappableLabelDelegate? override init(frame: CGRect) { super.init(frame: frame) commonInit() } required init?(coder: NSCoder) { super.init(coder: coder) } override func awakeFromNib() { super.awakeFromNib() commonInit() } private func commonInit() { self.textAlignment = .left // has to be left for this to work! addGestureRecognizers() } private func addGestureRecognizers() { let tap = UITapGestureRecognizer(target: self, action: #selector(tappedLabel(_:))) self.isUserInteractionEnabled = true self.addGestureRecognizer(tap) } @objc private func tappedLabel(_ tap: UITapGestureRecognizer) { guard let label = tap.view as? TappableLabel, label == self, tap.state == .ended else { return } let location = tap.location(in: label) processInteraction(at: location, wasTap: true) } private func processInteraction(at location: CGPoint, wasTap: Bool) { let label = self guard let attributedText = label.attributedText else { return // nothing to do } let textStorage = NSTextStorage(attributedString: attributedText) let textContainer = NSTextContainer(size: label.bounds.size) let layoutManager = NSLayoutManager() layoutManager.addTextContainer(textContainer) textStorage.addLayoutManager(layoutManager) textContainer.lineFragmentPadding = 0.0 textContainer.lineBreakMode = label.lineBreakMode textContainer.maximumNumberOfLines = label.numberOfLines let characterIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil) if characterIndex < textStorage.length { log.info("Character Index: \(characterIndex)") let range = NSRange(location: characterIndex, length: 1) let substring = (attributedText.string as NSString).substring(with: range) log.info("Character at Index: \(substring)") if let labelLink = attributedText.attribute(.custom, at: characterIndex, effectiveRange: nil) as? LabelLink { log.debug("You \(wasTap ? "tapped" : "pressed") on \(labelLink.text) and the value is: \(String(describing: labelLink.link))") self.delegate?.didTapOnLink(labelLink, in: self) } } } // will set the label's text to the given text argument, but for any callbackString it will search the text for that and embed it. func setText(_ text: String, withCallbacksOn callbackStrings: [LabelLink] = []) { self.text = text let attributedString = NSMutableAttributedString(string: text) let coreAttributes: [NSAttributedString.Key: Any] = [ .foregroundColor : self.textColor!, .font: self.font! ] attributedString.setAttributes(coreAttributes, range: NSRange(location: 0, length: text.count)) for labelLink in callbackStrings { let range = (text as NSString).range(of: labelLink.text) if range.location != NSNotFound { var additionalAttributes = coreAttributes additionalAttributes[.custom] = labelLink attributedString.setAttributes(additionalAttributes, range: range) } } self.attributedText = attributedString } }