【问题标题】:How to detect touch on NSTextAttachment如何检测 NSTextAttachment 上的触摸
【发布时间】:2013-10-19 12:44:37
【问题描述】:

在 iOS 上检测用户何时点击 NSTextAttachment 的最佳方法是什么?

我认为其中一种方法是检查 carret 位置上的字符是否为 NSAttachmentCharacter,但它似乎不正确。

我也尝试过UITextViewDelegate 方法:-(BOOL)textView:(UITextView *)textView shouldInteractWithTextAttachment:(NSTextAttachment *)textAttachment inRange:(NSRange)characterRange 但在textView.editable=YES 时它没有被调用

【问题讨论】:

  • 嗨,Michal,我现在遇到了完全相同的问题,并且希望能够检测到 NSTextAttachment 上的点击和触摸,即使 textView.editable=YES 也是如此 - 你找到解决方案了吗?

标签: ios objective-c ios7 nstextattachment


【解决方案1】:

乔希的answer 几乎是完美的。但是,如果您在输入末尾之后点击 UITextView 的空格,glyphIndex(for:in:fractionOfDistanceThroughGlyph) 将返回字符串中的最终字形。如果这是您的附件,它将错误地评估为真。

Apple 的文档说:如果点下没有字形,则返回最近的字形,其中最近的字形根据鼠标选择的要求定义。希望确定该点是否实际上位于返回的字形范围内的客户应在此之后调用 boundingRect(forGlyphRange:in:) 并测试该点是否落在该方法返回的矩形内。

所以,这是一个调整后的版本(Swift 5、XCode 10.2),它对检测到的字形的边界进行了额外的检查。我相信一些 characterIndex 测试现在是多余的,但它们不会伤害任何东西。

一个警告:字形似乎延伸到包含它们的行的高度。如果您在横向图片附件旁边有一个纵向图片附件,则点击横向图片上方的空白仍然会评估为真。

import UIKit
import UIKit.UIGestureRecognizerSubclass

// Thanks to https://stackoverflow.com/a/52883387/658604
// and https://stackoverflow.com/a/49153247/658604

/// Recognizes a tap on an attachment, on a UITextView.
/// The UITextView normally only informs its delegate of a tap on an attachment if the text view is not editable, or a long tap is used.
/// If you want an editable text view, where you can short cap an attachment, you have a problem.
/// This gesture recognizer can be added to the text view, and will add requirments in order to recognize before any built-in recognizers.
class AttachmentTapGestureRecognizer: UITapGestureRecognizer {

    typealias TappedAttachment = (attachment: NSTextAttachment, characterIndex: Int)

    private(set) var tappedState: TappedAttachment?

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
        tappedState = nil

        guard let textView = view as? UITextView else {
            state = .failed
            return
        }

        if let touch = touches.first {
            tappedState = evaluateTouch(touch, on: textView)
        }

        if tappedState != nil {
            // UITapGestureRecognizer can accurately differentiate discrete taps from scrolling
            // Therefore, let the super view evaluate the correct state.
            super.touchesBegan(touches, with: event)

        } else {
            // User didn't initiate a touch (tap or otherwise) on an attachment.
            // Force the gesture to fail.
            state = .failed
        }
    }

    /// Tests to see if the user has tapped on a text attachment in the target text view.
    private func evaluateTouch(_ touch: UITouch, on textView: UITextView) -> TappedAttachment? {
        let point = touch.location(in: textView)
        let glyphIndex: Int = textView.layoutManager.glyphIndex(for: point, in: textView.textContainer, fractionOfDistanceThroughGlyph: nil)
        let glyphRect = textView.layoutManager.boundingRect(forGlyphRange: NSRange(location: glyphIndex, length: 1), in: textView.textContainer)
        guard glyphRect.contains(point) else {
            return nil
        }
        let characterIndex: Int = textView.layoutManager.characterIndexForGlyph(at: glyphIndex)
        guard characterIndex < textView.textStorage.length else {
            return nil
        }
        guard NSTextAttachment.character == (textView.textStorage.string as NSString).character(at: characterIndex) else {
            return nil
        }
        guard let attachment = textView.textStorage.attribute(.attachment, at: characterIndex, effectiveRange: nil) as? NSTextAttachment else {
            return nil
        }
        return (attachment, characterIndex)
    }
}

【讨论】:

    【解决方案2】:

    委托方法确实有效,但前提是附件在图像属性中有图像并且可编辑 = 否!因此,如果您从其他地方将图像粘贴到属性字符串中,则数据似乎最终存储在 fileWrapper 中,下次您将属性字符串放回 textView 时,图像属性为 nil 并且布局管理器或任何获取图像的东西从文件包装器。

    在文档中的某处确实提到了 NSTextAttachment 中没有用于持久化图像属性的方法。

    要对此进行测试,请尝试从照片应用程序中复制一张照片并将其粘贴到您的 textView 中,现在如果您按住手指,您应该会看到默认菜单弹出。现在,如果你保存这个富文本,说成一个核心数据实体然后检索它,图像属性将为 nil,但图像数据将在attachment.fileWrapper.regularFileContents

    这很痛苦,我很想知道工程师的意图。所以你似乎有两个选择。

    1. 创建您自己的自定义 NSTextAttachment 并包含用于存档图像和其他设置的方法(当您解决这个问题时,请告诉我如何操作)
    2. 每次将字符串放回 textView 之前,您都会找到所有附件并重新创建图像属性,如下所示:

      attachment.image = [UIImage imageWithData:attachment.fileWrapper.regularFileContents];

    请记住,这样做的副作用是使 fileWrapper 无效。我想调整图像大小,但也要保留原始图像,这样我就不会失去完整的分辨率。我认为这样做的唯一方法可能是继承 NSTextAttachment。

    编辑:

    我想出了如何创建自定义的 NSTextAttachments - 这里有一个链接供感兴趣的人使用 http://ossh.com.au/design-and-technology/software-development/implementing-rich-text-with-images-on-os-x-and-ios/

    编辑 2:要在编辑模式下自定义菜单,请参阅以下 Apple 文档,问题是“touchEnded”似乎从未被调用,因此您可能不得不尝试使用 touchesBegan。小心不要干扰默认的编辑行为。

    https://developer.apple.com/library/ios/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/AddingCustomEditMenuItems/AddingCustomEditMenuItems.html

    请注意,在下面的代码中,您需要在// selection management 注释之后添加代码以确定触摸了哪个字符,检查它是否是特殊文本附件字符并 然后修改编辑菜单或采取其他措施。

    - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
        UITouch *theTouch = [touches anyObject];
    
        if ([theTouch tapCount] == 2  && [self becomeFirstResponder]) {
    
            // selection management code goes here...
    
            // bring up edit menu.
            UIMenuController *theMenu = [UIMenuController sharedMenuController];
            CGRect selectionRect = CGRectMake (currentSelection.x, currentSelection.y, SIDE, SIDE);
            [theMenu setTargetRect:selectionRect inView:self];
            [theMenu setMenuVisible:YES animated:YES];
    
        }
    }
    

    您也可以通过添加菜单项然后修改 canPerformAction 方法来添加自定义菜单。

    - (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
        LOG(@"canPerformAction: called");
    
        if (action == @selector(viewImage)) {
           // Check the selected character is the special text attachment character
    
           return YES;
        }
       return NO;
    }
    

    这是一些附加代码,但有点繁琐。第二种方法只是在检测到附件时禁用默认编辑菜单。

    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
        FLOG(@"touchesBegan:withEvent: called");
    
        if (self.selectedRange.location != NSNotFound) {
            FLOG(@" selected location is %d", self.selectedRange.location);
    
            int ch;
    
            if (self.selectedRange.location >= self.textStorage.length) {
                // Get the character at the location
                ch = [[[self textStorage] string] characterAtIndex:self.selectedRange.location-1];
            } else {
                // Get the character at the location
                ch = [[[self textStorage] string] characterAtIndex:self.selectedRange.location];
            }
    
            if (ch == NSAttachmentCharacter) {
                FLOG(@" selected character is %d, a TextAttachment", ch);
            } else {
                FLOG(@" selected character is %d", ch);
            }
        }
    
    }
    - (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
        FLOG(@"canPerformAction: called");
    
            FLOG(@" selected location is %d", self.selectedRange.location);
            FLOG(@" TextAttachment character is %d", NSAttachmentCharacter);
    
            if (self.selectedRange.location != NSNotFound) {
    
                int ch;
    
                if (self.selectedRange.location >= self.textStorage.length) {
                    // Get the character at the location
                    ch = [[[self textStorage] string] characterAtIndex:self.selectedRange.location-1];
                } else {
                    // Get the character at the location
                    ch = [[[self textStorage] string] characterAtIndex:self.selectedRange.location];
                }
    
                if (ch == NSAttachmentCharacter) {
                    FLOG(@" selected character is %d, a TextAttachment", ch);
                    return NO;
                } else {
                    FLOG(@" selected character is %d", ch);
                }
    
                // Check for an attachment
                NSTextAttachment *attachment = [[self textStorage] attribute:NSAttachmentAttributeName atIndex:self.selectedRange.location effectiveRange:NULL];
                if (attachment) {
                    FLOG(@" attachment attribute retrieved at location %d", self.selectedRange.location);
                    return NO;
                }
                else
                    FLOG(@" no attachment at location %d", self.selectedRange.location);
            }
        return [super canPerformAction:action withSender:sender];
    }
    

    【讨论】:

    • 感谢您的回答,但我认为最初的问题是即使设置了textView.editable = YES,如何检测触摸。
    • 这可以使用与显示文本编辑弹出菜单相同的方法。您必须检查所选字符是否是特殊文本附件字符 - 我会看看是否可以找到示例。
    • @DuncanGroenewald - 这个答案的大部分似乎与这个问题完全无关。
    • -1:完全垃圾。这不会检测您是否触摸了附件。它检测光标之后的字符(或光标之前的字符,如果光标位于末尾)是否为附件。如果您的附件位于正文的中间,并且您在它之后选择了它,它会说您没有选择它。
    • 强词。这可能是一个选项stackoverflow.com/questions/20203690/…
    【解决方案3】:

    Apple 让这变得非常困难。正如其他人指出的那样,委托方法被调用,但仅当isEditablefalse 时,或者当用户点击并按住附件时。如果您想在编辑过程中了解简单的点击交互,请忽略它。

    我沿着touchesBegan:hitTest: 路径走下去,都遇到了问题。 UITextView 已经处理了交互之后调用touches 方法,而hitTest: 过于粗略,因为它会与第一响应者状态等混淆。

    我最终的解决方案是手势识别器。 Apple 正在内部使用这些,这就解释了为什么 touchesBegan: 首先并不真正可行:手势识别器已经处理了该事件。

    我创建了一个新的手势识别器类,用于UITextView。它只是检查水龙头的位置,如果它是附件,它会处理它。我让所有其他手势识别器从属于我的手势识别器,因此我们首先查看事件,而其他手势识别器只有在我们的手势识别器失败时才会发挥作用。

    手势识别器类如下,以及将其添加到UITextView 的扩展。我在awakeFromNibUITextView 子类中添加它,就像这样。 (如果没有子类,则无需使用子类。)

    override func awakeFromNib() {
        super.awakeFromNib()
    
        let recognizer = AttachmentTapGestureRecognizer(target: self, action: #selector(handleAttachmentTap(_:)))
        add(recognizer)
    

    我通过调用现有的UITextViewDelegate 方法textView(_:,shouldInteractWith:,in:,interaction:) 来处理该操作。您可以轻松地将处理代码直接放在操作中,而不是使用委托。

    @IBAction func handleAttachmentTap(_ sender: AttachmentTapGestureRecognizer) {
        let _ = delegate?.textView?(self, shouldInteractWith: sender.attachment!, in: NSRange(location: sender.attachmentCharacterIndex!, length: 1), interaction: .invokeDefaultAction)
    }
    

    这里是主类。

    import UIKit
    import UIKit.UIGestureRecognizerSubclass
    
    /// Recognizes a tap on an attachment, on a UITextView.
    /// The UITextView normally only informs its delegate of a tap on an attachment if the text view is not editable, or a long tap is used.
    /// If you want an editable text view, where you can short cap an attachment, you have a problem.
    /// This gesture recognizer can be added to the text view, and will add requirments in order to recognize before any built-in recognizers.
    class AttachmentTapGestureRecognizer: UIGestureRecognizer {
    
        /// Character index of the attachment just tapped
        private(set) var attachmentCharacterIndex: Int?
    
        /// The attachment just tapped
        private(set) var attachment: NSTextAttachment?
    
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
            attachmentCharacterIndex = nil
            attachment = nil
    
            let textView = view as! UITextView
            if touches.count == 1, let touch = touches.first, touch.tapCount == 1 {
                let point = touch.location(in: textView)
                let glyphIndex: Int? = textView.layoutManager.glyphIndex(for: point, in: textView.textContainer, fractionOfDistanceThroughGlyph: nil)
                let index: Int? = textView.layoutManager.characterIndexForGlyph(at: glyphIndex ?? 0)
                if let characterIndex = index, characterIndex < textView.textStorage.length {
                    if NSAttachmentCharacter == (textView.textStorage.string as NSString).character(at: characterIndex) {
                        attachmentCharacterIndex = characterIndex
                        attachment = textView.textStorage.attribute(.attachment, at: characterIndex, effectiveRange: nil) as? NSTextAttachment
                        state = .recognized
                    } else {
                        state = .failed
                    }
                }
            } else {
                state = .failed
            }
        }
    }
    
    extension UITextView {
    
        /// Add an attachment recognizer to a UITTextView
        func add(_ attachmentRecognizer: AttachmentTapGestureRecognizer) {
            for other in gestureRecognizers ?? [] {
                other.require(toFail: attachmentRecognizer)
            }
            addGestureRecognizer(attachmentRecognizer)
        }
    
    }
    

    这种方法大概也可以用于点击链接。

    【讨论】:

    【解决方案4】:

    我已将 Drew 的手势识别器 here 更改为子类 UITapGestureRecognizer 而不是 UIGestureRecognizer

    这提供了一个优势,即它只检测离散的点击而不是滚动的开始。

    import UIKit
    import UIKit.UIGestureRecognizerSubclass
    
    // Modified from: https://stackoverflow.com/a/49153247/658604
    
    /// Recognizes a tap on an attachment, on a UITextView.
    /// The UITextView normally only informs its delegate of a tap on an attachment if the text view is not editable, or a long tap is used.
    /// If you want an editable text view, where you can short cap an attachment, you have a problem.
    /// This gesture recognizer can be added to the text view, and will add requirments in order to recognize before any built-in recognizers.
    class AttachmentTapGestureRecognizer: UITapGestureRecognizer {
    
        typealias TappedAttachment = (attachment: NSTextAttachment, characterIndex: Int)
    
        private(set) var tappedState: TappedAttachment?
    
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
            tappedState = nil
    
            guard let textView = view as? UITextView else {
                state = .failed
                return
            }
    
            if let touch = touches.first {
                tappedState = evaluateTouch(touch, on: textView)
            }
    
            if tappedState != nil {
                // UITapGestureRecognizer can accurately differentiate discrete taps from scrolling
                // Therefore, let the super view evaluate the correct state.
                super.touchesBegan(touches, with: event)
    
            } else {
                // User didn't initiate a touch (tap or otherwise) on an attachment.
                // Force the gesture to fail.
                state = .failed
            }
        }
    
        /// Tests to see if the user has tapped on a text attachment in the target text view.
        private func evaluateTouch(_ touch: UITouch, on textView: UITextView) -> TappedAttachment? {
            let point = touch.location(in: textView)
            let glyphIndex: Int? = textView.layoutManager.glyphIndex(for: point, in: textView.textContainer, fractionOfDistanceThroughGlyph: nil)
            let index: Int? = textView.layoutManager.characterIndexForGlyph(at: glyphIndex ?? 0)
            guard let characterIndex = index, characterIndex < textView.textStorage.length else {
                return nil
            }
            guard NSTextAttachment.character == (textView.textStorage.string as NSString).character(at: characterIndex) else {
                return nil
            }
            guard let attachment = textView.textStorage.attribute(.attachment, at: characterIndex, effectiveRange: nil) as? NSTextAttachment else {
                return nil
            }
            return (attachment, characterIndex)
        }
    }
    

    【讨论】:

    • 感谢乔希的精彩回答。我做了一个小改进,以更好地处理输入字符串末尾的附件:stackoverflow.com/a/55854337/1758224
    • @Josh-Fuggle 我很难使用你的代码。我复制过去并运行它会出错。我用的是swift 3,能指导一下吗?
    【解决方案5】:

    斯威夫特 3 答案:

    func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: NSRange) -> Bool {
        return true
    }
    

    确保您的 textView isEditable = falseisSelectable = trueisUserInteractionEnabled = true。邓肯的回答没有提到isUserInteractionEnabled,这个必须是true,不然不行。

    您可以通过编程方式 (textView.isEditable = false) 或通过属性检查器执行此操作:

    【讨论】:

      【解决方案6】:

      SWIFT 4.2

      @objc func myMethodToHandleTap(_ sender: UITapGestureRecognizer) {
      
      let myTextView = sender.view as! UITextView
      
           let layoutManager = myTextView.layoutManager
      
           // location of tap in myTextView coordinates and taking the inset into account
           var location = sender.location(in: myTextView)
           location.x -= myTextView.textContainerInset.left;
           location.y -= myTextView.textContainerInset.top;
      
           // character index at tap location
           let characterIndex = layoutManager.characterIndex(for: location, in: myTextView.textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
      
           if characterIndex < myTextView.textStorage.length {
      
              let attributeValue = myTextView.attributedText.attribute(NSAttributedString.Key.attachment, at: characterIndex, effectiveRange: nil) as? NSTextAttachment
      
              if let _ = attributeValue {
                 print("TAPPED ATTACHMENT")  
              }
           }
      
      }
      

      【讨论】:

        【解决方案7】:

        使用 hitTest 在子类 UITextView 中获取触摸。这避免了弄乱标准编辑功能的问题。从位置获取字符索引,然后检查附件的字符。

        【讨论】:

          【解决方案8】:

          我发现在属性字符串中解决这种点击附件的常用方法是使用 UITextView 。但是 UITextView 非常复杂,无法禁用所有菜单、缩放等。所以我做了一个简单版本的 UILabel 包装器来支持附件点击。它简单易用,易于更改。演示在这里:

          self.attachmentLabel.attributeText = attributeText;
          self.attachmentLabel.numberOflines = 2;
          self.attachmentLabel.selectBlock = ^(NSInteger attachmentIndex) {
              NSLog(@"attachment:%ld called",attachmentIndex);
          };
          

          https://github.com/ximmyxiao/TestAttributeStringClick

          希望对你有帮助:)

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2011-06-11
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2022-10-02
            • 1970-01-01
            相关资源
            最近更新 更多