My eventual solution was to swap out spaces for blank images (blanks) that have the adjusted width.
Basic components:
a) Method to replace spaces with blanks
b) Method to replace blanks with spaces
c) NSValueTransformer for the NSTextView to do (a) for transformedValue and (b) for reverseTransformedValue
d) NSTextViewDelegate to do (a) when the text changes
e) Subclass NSTextView to do (b) on copied or cut text before sending to pasteboard
f) Action assigned to the stepper to make the size changes
Code for each part is below:
a) AppDelegate method to replace spaces with blanks
- (NSAttributedString * ) replaceSpacesWithBlanks:(NSString *)replaceString {
CGFloat imageWidth = [[NSUserDefaults standardUserDefaults] integerForKey:@"wordSpacing"];
NSImage * pic = [[NSImage alloc] initWithSize:NSMakeSize(imageWidth, 1.0f)];
NSTextAttachmentCell *attachmentCell = [[NSTextAttachmentCell alloc] initImageCell:pic];
NSTextAttachment *attachment = [[NSTextAttachment alloc] init];
[attachment setAttachmentCell: attachmentCell ];
NSAttributedString *replacementString = [NSAttributedString attributedStringWithAttachment: attachment];
NSMutableAttributedString *mutableString = [[NSMutableAttributedString alloc] initWithString:replaceString];
NSRange range = [[mutableString string] rangeOfString:@" "];
while (range.location != NSNotFound) {
[mutableString replaceCharactersInRange:range withAttributedString:replacementString];
range = [[mutableString string] rangeOfString:@" "];
}
return [[NSAttributedString alloc] initWithAttributedString: mutableString];
}
b) AppDelegate method to replace blanks with spaces
- (NSString * ) replaceBlanksWithSpaces:(NSAttributedString *)replaceAttributedString {
NSMutableAttributedString * mutAttrString = [[NSMutableAttributedString alloc] initWithAttributedString:replaceAttributedString];
for (int index = 0; index < mutAttrString.length; index += 1) {
NSRange theRange;
NSDictionary * theAttributes = [mutAttrString attributesAtIndex:index effectiveRange:&theRange];
NSTextAttachment *theAttachment = [theAttributes objectForKey:NSAttachmentAttributeName];
if(theAttachment != NULL) {
[mutAttrString replaceCharactersInRange:theRange withString:@" "];
}
}
return mutAttrString.string;
}
c) NSValueTransformer for the NSTextView to replace spaces with blanks for transformedValue and replace blanks with spaces for reverseTransformedValue
@implementation DBAttributedStringTransformer
- (id)init
{
self = [super init];
if (self) {
appDelegate = (AppDelegate *)[[NSApplication sharedApplication] delegate];
}
return self;
}
+ (Class)transformedValueClass
{
return [NSAttributedString class];
}
+ (BOOL)allowsReverseTransformation
{
return YES;
}
- (id)transformedValue:(id)value
{
return [appDelegate replaceSpacesWithBlanks:value];
}
- (id)reverseTransformedValue:(id)value
{
return [appDelegate replaceBlanksWithSpaces:value];
}
d) NSTextViewDelegate to replace spaces with blanks when the text changes
@implementation DBTextViewDelegate
-(void)awakeFromNib {
appDelegate = (AppDelegate *)[[NSApplication sharedApplication] delegate];
}
- (void)textViewDidChangeSelection:(NSNotification *)aNotification{
// Need to keep track of where the cursor should be reinserted
textLength = myTextView.string.length;
insertionPoint = [[[myTextView selectedRanges] objectAtIndex:0] rangeValue].location;
}
//replaces spaces with blank image and puts cursor back in correct position
- (void)textDidChange:(NSNotification *)aNotification{
NSInteger newTextLength = myTextView.string.length;
NSInteger newInsertionPoint = insertionPoint + newTextLength - textLength;
NSString * stringValue = [[NSUserDefaults standardUserDefaults] stringForKey:@"textViewString"];
NSAttributedString * attrStringWithBlanks = [[ NSAttributedString alloc] initWithAttributedString:[appDelegate replaceSpacesWithBlanks:stringValue ]];
NSMutableAttributedString *mutableString = [[NSMutableAttributedString alloc] initWithAttributedString:attrStringWithBlanks];
[myTextView.textStorage setAttributedString: mutableString];
//Put the cursor back where it was
[myTextView setSelectedRange:NSMakeRange(newInsertionPoint, 0)];
}
e) Subclass NSTextView to replace blanks with spaces on copied or cut text before writing to pasteboard
@implementation DBTextView
-(void)awakeFromNib {
appDelegate = (AppDelegate *)[[NSApplication sharedApplication] delegate];
}
-(void) selectedTextToClipBoard{
NSRange selectedRange = [self selectedRange];
NSAttributedString * selectedText = [[self textStorage] attributedSubstringFromRange: selectedRange];
NSString * textWithoutBlanks = [appDelegate replaceBlanksWithSpaces:selectedText];
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
[pasteboard clearContents];
NSArray *copiedObject = [NSArray arrayWithObject:textWithoutBlanks];
[pasteboard writeObjects:copiedObject];
}
-(void) copy:(id)sender{
[self selectedTextToClipBoard];
}
-(void) cut:(id)sender{
[self selectedTextToClipBoard];
// Delete selected text so it acts like a cut
NSRange selectedRange = [self selectedRange];
[[self textStorage] deleteCharactersInRange:selectedRange];
}
f) Action assigned to the stepper to make the size changes
- (IBAction)changeWordSpacing:(id)sender {
CGFloat imageWidth = [[NSUserDefaults standardUserDefaults] integerForKey:@"wordSpacing"];
NSImage * pic = [[NSImage alloc] initWithSize:NSMakeSize(imageWidth, 1.0f)];
NSTextAttachmentCell *attachmentCell = [[NSTextAttachmentCell alloc] initImageCell:pic];
NSMutableAttributedString * mutAttrString = [[NSMutableAttributedString alloc] initWithAttributedString:[textView textStorage]];
for (int index = 0; index < mutAttrString.length; index += 1) {
NSRange theRange;
NSDictionary * theAttributes = [mutAttrString attributesAtIndex:index effectiveRange:&theRange];
NSTextAttachment *theAttachment = [theAttributes objectForKey:NSAttachmentAttributeName];
if(theAttachment != NULL) {
[theAttachment setAttachmentCell: attachmentCell ];
}
}
[[textView textStorage] setAttributedString:mutAttrString];
}
Also, NSTextView should be set to “Continuously Updates Value”