为UILabel指定字符串添加UITapGestureRecognizer点击区域异常问题
问题分析与解决方案
这个问题的核心原因是你在UITapGestureRecognizer扩展中创建的NSTextContainer和UILabel内部实际使用的文本容器配置不一致,导致点击位置的计算出现偏移,最终表现为只有部分字符可点击,且不同设备上的偏移程度不同。
具体问题点
UILabel内部的NSTextContainer默认带有lineFragmentPadding = 4.0(文本左右的额外内边距),但你在扩展中手动设置了textContainer.lineFragmentPadding = 0.0,这就使得你模拟的文本布局和Label实际显示的文本布局位置不匹配,点击坐标转换后得到的字符索引自然就不准确了。
另外,还有一个潜在风险:你通过label.text来获取目标字符串的范围,但如果attributedText和text的内容出现不一致(比如富文本中存在特殊字符或排版差异),也会导致范围判断错误。
修正后的代码
1. 修复UITapGestureRecognizer扩展的点击位置计算
调整lineFragmentPadding匹配UILabel的默认值,并优化坐标计算逻辑:
extension UITapGestureRecognizer { func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool { guard let attributedText = label.attributedText else { return false } // 匹配UILabel内部的textContainer配置 let layoutManager = NSLayoutManager() let textContainer = NSTextContainer(size: label.bounds.size) let textStorage = NSTextStorage(attributedString: attributedText) layoutManager.addTextContainer(textContainer) textStorage.addLayoutManager(layoutManager) // 关键:设置与UILabel一致的lineFragmentPadding(默认4.0) textContainer.lineFragmentPadding = 4.0 textContainer.lineBreakMode = label.lineBreakMode textContainer.maximumNumberOfLines = label.numberOfLines textContainer.size = label.bounds.size // 计算点击位置在文本容器中的坐标 let touchLocation = location(in: label) let textBoundingBox = layoutManager.usedRect(for: textContainer) let textOffset = CGPoint( x: (label.bounds.width - textBoundingBox.width) * 0.5 - textBoundingBox.origin.x, y: (label.bounds.height - textBoundingBox.height) * 0.5 - textBoundingBox.origin.y ) let touchPointInContainer = CGPoint( x: touchLocation.x - textOffset.x, y: touchLocation.y - textOffset.y ) // 获取点击对应的字符索引 guard let index = layoutManager.characterIndex(for: touchPointInContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil) as? Int else { return false } return NSLocationInRange(index, targetRange) } }
2. 优化点击事件中的范围获取逻辑
直接从attributedText中获取目标字符串的范围,避免label.text和attributedText不一致的问题:
@objc func tappedOnLabel(_ gesture: UITapGestureRecognizer) { guard let attributedText = self.documentsLabel.attributedText else { return } let fullText = attributedText.string as NSString let nutzenRange = fullText.range(of: "Nutzungsbedingungen") let datenRange = fullText.range(of: "Datenschutzrichtlinien") if gesture.didTapAttributedTextInLabel(label: self.documentsLabel, inRange: nutzenRange) { showAlert(withMessage: "user tapped on Nutzungsbedingungen") } else if gesture.didTapAttributedTextInLabel(label: self.documentsLabel, inRange: datenRange) { showAlert(withMessage: "user tapped on Datenschutzrichtlinien") } } // 抽弹框逻辑简化代码 private func showAlert(withMessage message: String) { let alertcontroller = UIAlertController(title: "Tapped on", message: message, preferredStyle: .alert) alertcontroller.addAction(UIAlertAction(title: "OK", style: .default)) present(alertcontroller, animated: true) }
3. 可选优化:给可点击文本添加下划线(提升用户体验)
在getAttributedString方法中,给可点击的文本添加下划线,让用户明确知道这些文字是可交互的:
func getAttributedString(arrayText:[String]?, arrayColors:[UIColor]?, arrayFonts:[UIFont]?) -> NSMutableAttributedString { let finalAttributedString = NSMutableAttributedString() guard let texts = arrayText, let colors = arrayColors, let fonts = arrayFonts, texts.count == colors.count, texts.count == fonts.count else { return finalAttributedString } for i in 0 ..< texts.count { var attributes = [NSAttributedString.Key: Any]() attributes[.foregroundColor] = colors[i] attributes[.font] = fonts[i] // 给目标文本添加下划线 if texts[i] == "Nutzungsbedingungen" || texts[i] == "Datenschutzrichtlinien" { attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue } let attributedStr = NSAttributedString(string: texts[i], attributes: attributes) if i != 0 { finalAttributedString.append(NSAttributedString(string: " ")) } finalAttributedString.append(attributedStr) } return finalAttributedString }
验证效果
修改后,不同设备上的可点击区域应该能完全覆盖目标字符串,点击任意位置都会触发对应的弹窗。另外,下划线的添加也能让用户更清晰地识别可交互文本。
内容的提问来源于stack exchange,提问作者Chris
相关产品推荐
相关产品推荐

