iosobjective-cios7nstextattachment

How to detect touch on NSTextAttachment


What is the best way to detect when user taps on NSTextAttachment on iOS?

I think that one of the ways would be checking for the character on carret's position whether it is NSAttachmentCharacter, but it just doesn't seem right.

I've also tried UITextViewDelegate method: -(BOOL)textView:(UITextView *)textView shouldInteractWithTextAttachment:(NSTextAttachment *)textAttachment inRange:(NSRange)characterRange but it's not invoked when textView.editable=YES


Solution

  • The delegate method does work but ONLY if the attachment has an image in the image attribute AND if editable = NO! So if you have an image pasted in to the attributedString from somewhere else it seems the data ends up being stored in the fileWrapper and next time you put the attributedString back into the textView the image attribute is nil and the layout manager or whatever gets the image from the fileWrapper.

    Somewhere in the documents it does mention that there are no methods in NSTextAttachment for persistence of the image attribute.

    To test this try copy a photo from the Photo app and paste it into your textView, now if you hold down your finger on it you should see the default menu pop up. Now if you save this rich text, say into a Core Data entity and then retrieve it the image attribute will be nil but the image data will be in attachment.fileWrapper.regularFileContents

    Its a pain, and I would love to know the engineers intention. So you have two options it seems.

    1. Create your own custom NSTextAttachment and include methods for archiving the image and other settings (PLEASE SHOW ME HOW TOO WHEN YOU FIGURE THIS ONE OUT)
    2. Every time prior to putting your string back into textView you find all the attachments and recreated the image attribute like so:

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

    Bear in mind the side effect of doing this is invalidating the fileWrapper. I want to resize the image but also keep the original so I don't loose the full resolution. I think the only way of doing this might be to subclass NSTextAttachment.

    EDIT:

    I figured out how to create the custom NSTextAttachments - here is a link for those interested http://ossh.com.au/design-and-technology/software-development/implementing-rich-text-with-images-on-os-x-and-ios/

    EDIT 2: To customise the menu when in Edit Mode see the following Apple documents, the issue is 'touchEnded' never seems to get called so you might have to try using touchesBegan. Careful you don't interfere with the default editing behaviour though.

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

    Note that in the code below you would need to add code after // selection management comment to determine which character was touched, check if it is the special text attachment character and then modify the edit menu or take some other action.

    - (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];
    
        }
    }
    

    Alternately you could add a custom menu by adding the menu item and then modifying the canPerformAction method.

    - (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;
    }
    

    Here is some addition code but its a bit fussy. Second method just disables the default edit menu if an attachment is detected.

    - (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];
    }