3
votes

I’m trying to make a reader application to help a girl with reading difficulties. Some research shows that just changing the colors of the text, background and shadow can really help kids out so I’m trying to allow her to do that. It’s just a big NSTextView with buttons so she can change the font size, color, background color, shadow properties, letter spacing, line spacing and word spacing. I know you can do most of this just using Word but I’m trying to make it as intuitive/fun as possible for her.

The place where I could use a hand is in changing the size of the spacing between words. Currently I’m just searching for a string of spaces equal to the number of spaces I expect to be there and then replacing with more or less spaces it as follows:

- (IBAction)increaseSpacing:(id)sender{ 

  NSInteger spacing = [[NSUserDefaults standardUserDefaults] integerForKey:@"wordSpacing"];
  NSMutableString * oldString = [ NSMutableString stringWithCapacity:0];
  NSMutableString * newString =[ NSMutableString stringWithCapacity:0];

  for (int i = 0; i < spacing; i+=1) {
    [oldString appendString:@" "];
  }

  [newString setString:oldString];
  [newString appendString:@" "];

  [[[textView textStorage] mutableString] replaceOccurrencesOfString:oldString
                           withString:newString options:0
                                range:NSMakeRange(0, [[textView textStorage] length])];

  spacing += 1;

  [[NSUserDefaults standardUserDefaults] setValue:[NSNumber numberWithInteger: spacing] forKey:@"wordSpacing"];

}

- (IBAction)reduceSpacing:(id)sender{

  NSInteger spacing = [[NSUserDefaults standardUserDefaults] integerForKey:@"wordSpacing"];

  if (spacing > 1) {

    NSMutableString * oldString = [ NSMutableString stringWithCapacity:0];
    NSMutableString * newString =[ NSMutableString stringWithCapacity:0];

    for (int i = 0; i < spacing-1; i+=1) {

      [newString appendString:@" "];

    }

    [oldString setString:newString];
    [oldString appendString:@" "];

    [[[textView textStorage] mutableString] replaceOccurrencesOfString:oldString
                                                        withString:newString options:0
                                                             range:NSMakeRange(0, [[textView textStorage] length])];
    spacing -= 1;
    [[NSUserDefaults standardUserDefaults] setValue:[NSNumber numberWithInteger: spacing] forKey:@"wordSpacing"];

  }

}

This approach feels sloppy to me, especially when moving the cursor around with arrow keys. I could just change the font size of a space character when it’s typed, but that would also change the line height. Is there a way that I can just change the width of the space character? Thanks in advance for your help.

1

1 Answers

0
votes

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”