You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 04:58:16