Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to subclass NSTextAttachment?

Here is my problem:

I use Core Data to store rich text input from iOS and/or OS X apps and would like images pasted into the NSTextView or UITextView to: a) retain their original resolution, and b) on display to be scaled to fit the textView correctly, which means scaling based on the size of the view on the device.

Currently I am using - (void)textStorage:(NSTextStorage *)textStorage didProcessEditing:(NSTextStorageEditActions)editedMask range:(NSRange)editedRange changeInLength:(NSInteger)delta to look for attachments and to then generate an image with a scale factor and assigning it to the textAttachment.image attribute.

This kind of works because I just change the scale factor and the original image gets retained but I believe a more elegant solution would be to use a NSTextAttachmentContainer subclass and to return from this an appropriately sized CGREct with

- (CGRect)attachmentBoundsForTextContainer:(NSTextContainer *)textContainer proposedLineFragment:(CGRect)lineFrag glyphPosition:(CGPoint)position characterIndex:(NSUInteger)charIndex

So my question is how do I create and insert such a subclass ?

Do I use the textStorage:didProcessEditing to iterate over each attachment and replace its NSTextAttachmentContainer with a class of my own, or can I simply create a Category and then somehow use this category to change the default behaviour. The latter seems much less intrusive but how do I get my textViews to automatically use this Category?

Oops: Just noticed NSTextAttachmentContainer is a protocol so I assume then creating a Category on NSTextAttachment and overriding the method above is an option.

Mmm: can't use Category to override an existing class method so I guess subclassing is the only option in which case how do I get the UITextView to use my attachment subclass, or do I have to iterate over the attributedString to replace all NSTextAttachments with instances of MYTextAttachment. And what will be the impact of unarchiving this string on OS X into say the default OS X NSTextAttachment (which is different from the iOS class) ?

like image 248
Duncan Groenewald Avatar asked Oct 30 '13 00:10

Duncan Groenewald


2 Answers

Based on this excellent article, if you want to make use of

- (CGRect)attachmentBoundsForTextContainer:(NSTextContainer *)textContainer proposedLineFragment:(CGRect)lineFrag glyphPosition:(CGPoint)position characterIndex:(NSUInteger)charIndex

to scale an image text attachment, you have to create your own subclass of NSTextAttachment

@interface MYTextAttachment : NSTextAttachment 
@end

with the scale operation in the implementation:

@implementation MYTextAttachment

- (CGRect)attachmentBoundsForTextContainer:(NSTextContainer *)textContainer proposedLineFragment:(CGRect)lineFrag glyphPosition:(CGPoint)position characterIndex:(NSUInteger)charIndex {
    CGFloat width = lineFrag.size.width;

    // Scale how you want
    float scalingFactor = 1.0;   
    CGSize imageSize = [self.image size];   
    if (width < imageSize.width)
        scalingFactor = width / imageSize.width;
    CGRect rect = CGRectMake(0, 0, imageSize.width * scalingFactor, imageSize.height * scalingFactor);

    return rect;
}
@end

based on

lineFrag.size.width

which give you (or what I have understood as) the width taken by the textView on which you have (will) set the attributed text "embedding" your custom text attachment.

Once the subclass of NSTextAttachment created, all you have to do is make use of it. Create an instance of it, set an image, then create a new attributed string with it and append it to a NSMutableAttributedText per example:

MYTextAttachment* _textAttachment = [MYTextAttachment new];
_textAttachment.image = [UIImage ... ];

[_myMutableAttributedString appendAttributedString:[NSAttributedString attributedStringWithAttachment:_immediateTextAttachment]];

For info it seems that

 - (CGRect)attachmentBoundsForTextContainer:(NSTextContainer *)textContainer proposedLineFragment:(CGRect)lineFrag glyphPosition:(CGPoint)position characterIndex:(NSUInteger)charIndex 

is called whenever the textview is asked to be relayout-ed.

Hope it helps, even though it doesn't answer every aspect of your problem.

like image 78
Bluezen Avatar answered Sep 21 '22 04:09

Bluezen


Swift 3 (based on @Bluezen's answer):

class MyTextAttachment : NSTextAttachment {

    override func attachmentBounds(for textContainer: NSTextContainer?, proposedLineFragment lineFrag: CGRect, glyphPosition position: CGPoint, characterIndex charIndex: Int) -> CGRect {

        guard let image = self.image else {
            return CGRect.zero
        }

        let height = lineFrag.size.height

        // Scale how you want
        var scalingFactor = CGFloat(0.8)
        let imageSize = image.size
        if height < imageSize.height {
            scalingFactor *= height / imageSize.height
        }
        let rect = CGRect(x: 0, y: 0, width: imageSize.width * scalingFactor, height: imageSize.height * scalingFactor)

        return rect
    }

}

Note that I am scaling based on height, as I was getting a lineFrag width value of 10000000 in my particular use case. Also note that I replaced scalingFactor = ... with scalingFactor *= ... so that I could use an additional, non-unity scaling factor (0.8 in this case).

like image 31
Dana Wheeler Avatar answered Sep 20 '22 04:09

Dana Wheeler