UITextView编辑状态下检测NSAttributedString图片点击的实现方案
解决UITextView编辑状态下的图片/视频缩略图点击检测问题
我来帮你搞定这个需求——你需要在UITextView处于编辑状态时,识别用户点击的图片(或视频缩略图)并触发播放逻辑,而原生的textView(_:shouldInteractWith:textAttachment:in:interaction:)方法只有在textView.isEditable = false时才会触发,确实有点棘手。下面是完整的解决方案,结合你的代码修改,完全能实现类似原生备忘录的效果:
核心思路
通过UITapGestureRecognizer获取点击坐标,利用LayoutManager将坐标转换为文本中的字符位置,再判断该位置是否包含NSTextAttachment,从而定位到被点击的图片/缩略图,最后触发对应的操作。
1. 完善手势点击的处理逻辑
你已经添加了TapGesture,现在只需要在tapDetected方法里补充判断逻辑,就能精准定位点击的附件:
@objc func tapDetected(sender: UITapGestureRecognizer) { guard let textView = sender.view as? UITextView else { return } let layoutManager = textView.layoutManager var location = sender.location(in: textView) // 调整点击位置,排除textContainer的内边距影响 location.x -= textView.textContainerInset.left location.y -= textView.textContainerInset.top let textContainer = textView.textContainer // 将坐标转换为文本中的字符索引 let characterIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil) // 防止索引越界 guard characterIndex < textView.textStorage.length else { return } // 获取该字符位置的所有属性 let attributes = textView.textStorage.attributes(at: characterIndex, effectiveRange: nil) // 判断是否包含NSTextAttachment if let textAttachment = attributes[.attachment] as? NSTextAttachment { print("点击了图片/视频缩略图!") // 显示缩略图(可选) imageView.image = textAttachment.image // 这里处理视频播放逻辑:从attachment的userInfo中取出视频URL if let videoURL = textAttachment.userInfo?["videoURL"] as? URL { print("准备播放视频:\(videoURL)") // 替换成你的视频播放代码,比如用AVPlayerViewController } } }
2. 给NSTextAttachment绑定视频信息
添加视频缩略图时,通过textAttachment.userInfo存储视频URL,这样点击时就能直接获取:
@IBAction func addVideoThumbnail(_ sender: Any) { let attributedString = NSMutableAttributedString(attributedString: textView.attributedText) let textAttachment = NSTextAttachment() // 设置视频缩略图 textAttachment.image = UIImage(named: "taylor") // 绑定视频URL到附件的userInfo textAttachment.userInfo = ["videoURL": URL(string: "https://example.com/your_video.mp4")!] // 调整图片大小适配textView宽度 let oldWidth = textAttachment.image!.size.width let scaleFactor = oldWidth / (textView.frame.size.width - 10) textAttachment.image = UIImage(cgImage: textAttachment.image!.cgImage!, scale: scaleFactor, orientation: .up) let attrStringWithAttachment = NSAttributedString(attachment: textAttachment) attributedString.append(attrStringWithAttachment) textView.attributedText = attributedString }
3. 修复手势冲突与无效代码
- 你已经设置了
gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:)返回true,这个是正确的,确保自定义手势和UITextView的原生编辑手势能同时工作。 - 移除无效的KVO监听:NSAttributedString不支持监听"image"属性,这些代码会导致无效输出甚至崩溃,直接删掉即可:
// 删掉以下无效代码 // textView.attributedText.addObserver(self, forKeyPath: "image", options: .new, context: nil) // textView.attributedText.addObserver(self, forKeyPath: "image", options: .initial, context: nil) // ...其他类似监听代码
4. 为什么exclusionPaths方案不可行?
你尝试用textContainer.exclusionPaths添加视图的方式,本质是把视图固定在某个位置,无法像NSTextAttachment那样嵌入文本流,自然不能随文本输入自动调整位置。所以正确的做法还是用NSTextAttachment来添加图片/缩略图。
修改后的完整ViewController代码
import UIKit class ViewController: UIViewController, UITextViewDelegate, UIGestureRecognizerDelegate { @IBOutlet var textView: UITextView! @IBOutlet var imageView: UIImageView! override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { textView.resignFirstResponder() print("touchesBegan") } override func viewDidLoad() { super.viewDidLoad() let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapDetected(sender:))) tapGesture.delegate = self textView.addGestureRecognizer(tapGesture) textView.delegate = self } func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true } @IBAction func addVideoThumbnail(_ sender: Any) { let attributedString = NSMutableAttributedString(attributedString: textView.attributedText) let textAttachment = NSTextAttachment() textAttachment.image = UIImage(named: "taylor") // 绑定视频URL textAttachment.userInfo = ["videoURL": URL(string: "https://example.com/your_video.mp4")!] let oldWidth = textAttachment.image!.size.width let scaleFactor = oldWidth / (textView.frame.size.width - 10) textAttachment.image = UIImage(cgImage: textAttachment.image!.cgImage!, scale: scaleFactor, orientation: .up) let attrStringWithAttachment = NSAttributedString(attachment: textAttachment) attributedString.append(attrStringWithAttachment) textView.attributedText = attributedString } @objc func tapDetected(sender: UITapGestureRecognizer) { guard let textView = sender.view as? UITextView else { return } let layoutManager = textView.layoutManager var location = sender.location(in: textView) location.x -= textView.textContainerInset.left location.y -= textView.textContainerInset.top let textContainer = textView.textContainer let characterIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil) guard characterIndex < textView.textStorage.length else { return } let attributes = textView.textStorage.attributes(at: characterIndex, effectiveRange: nil) if let textAttachment = attributes[.attachment] as? NSTextAttachment { print("点击了附件") imageView.image = textAttachment.image // 播放视频逻辑 if let videoURL = textAttachment.userInfo?["videoURL"] as? URL { print("准备播放视频:\(videoURL)") // 这里替换为你的视频播放代码 } } } // MARK: - UITextViewDelegate func textViewDidChange(_ textView: UITextView) { print("textViewDidChange") } func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { print("textViewShouldBeginEditing") return true } func textViewDidBeginEditing(_ textView: UITextView) { print("textViewDidBeginEditing") } func textViewDidEndEditing(_ textView: UITextView) { print("textViewDidEndEditing") } func textViewShouldEndEditing(_ textView: UITextView) -> Bool { print("textViewShouldEndEditing") return true } func textViewDidChangeSelection(_ textView: UITextView) { print("textViewDidChangeSelection") } func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { print("textView(_:shouldChangeTextIn:replacementText:)") return true } // 该方法仅在textView不可编辑时触发,编辑状态下不会执行 func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { print("textView(_:shouldInteractWith:textAttachment:in:interaction:)") imageView.image = textAttachment.image return true } } extension UITextView { func getParts() -> [AnyObject] { var parts = [AnyObject]() let attributedString = self.attributedText let range = self.selectedRange attributedString?.enumerateAttributes(in: range, options: []) { (object, range, stop) in if let attachment = object[.attachment] as? NSTextAttachment { if let image = attachment.image { parts.append(image) } else if let image = attachment.image(forBounds: attachment.bounds, textContainer: nil, characterIndex: range.location) { parts.append(image) } } else { let stringValue = attributedString!.attributedSubstring(from: range).string if !stringValue.trimmingCharacters(in: .whitespaces).isEmpty { parts.append(stringValue as AnyObject) } } } return parts } }
这样修改后,你就能在UITextView编辑状态下准确识别用户点击的视频缩略图,并触发播放逻辑,完全符合原生备忘录的交互效果。
内容的提问来源于stack exchange,提问作者Zafar Ahmad
相关产品推荐
相关产品推荐

