如何检测NSTextAttachment上的触摸

Mic*_*gar 16 objective-c nstextattachment ios ios7

检测用户何时点击NSTextAttachmentiOS 的最佳方法是什么?

我认为,其中一种方法是检查Carret的位置是否有NSAttachmentCharacter,但它似乎并不正确.

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

Eli*_*rke 5

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

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

因此,这里有一个经过调整的版本(Swift 5,XCode 10.2),它对检测到的字形的边界执行额外的检查。我相信一些characterIndex测试现在是多余的,但它们不会伤害任何东西。

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

import UIKit
import UIKit.UIGestureRecognizerSubclass

// Thanks to /sf/answers/3701837121/
// and /sf/answers/3440727321/

/// 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)
    }
}
Run Code Online (Sandbox Code Playgroud)


Dun*_*ald 3

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

在文档中的某个地方,它确实提到 NSTextAttachment 中没有用于持久保存图像属性的方法。

要测试此功能,请尝试从照片应用程序复制照片并将其粘贴到文本视图中,现在如果您按住手指,您应该会看到弹出的默认菜单。现在,如果您保存此富文本,例如保存到核心数据实体中,然后检索它,图像属性将为零,但图像数据将在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-和-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];

    }
}
Run Code Online (Sandbox Code Playgroud)

或者,您可以通过添加菜单项然后修改 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;
}
Run Code Online (Sandbox Code Playgroud)

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

- (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];
}
Run Code Online (Sandbox Code Playgroud)