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) ?
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.
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).