Add attributes to NSMutableAttributedString character by character

duci9y picture duci9y · Jun 23, 2013 · Viewed 17.5k times · Source

Here's my situation:

I have an NSMutableAttributedString with no attributes in a text view. Whenever the user presses the backspace key, I do not want a character to be deleted, I want it to be struck through, just like the "Track Changes" feature of productivity suites. I want the user to be able to continue typing normally after that. Here's how I started out:

- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
if (text.length == 0 && textView.text.length == 0) return YES;

if (text.length == 0 && !([[textView.text substringFromIndex:textView.text.length - 1] isEqualToString:@" "] || [[textView.text substringFromIndex:textView.text.length - 1] isEqualToString:@"\n"])) {

    textView.attributedText = [self strikeText:textView.attributedText];

    textView.selectedRange = NSMakeRange(textView.attributedText.length - 1, 0);

    return NO;
}
return YES;
}

- (NSAttributedString *)strikeText:(NSAttributedString *)text
{
NSRange range;
NSMutableAttributedString *returnValue = [[NSMutableAttributedString alloc] initWithAttributedString:text];

if (![text attribute:NSStrikethroughStyleAttributeName atIndex:text.length - 1 effectiveRange:&range]) {
    NSLog(@"%@", NSStringFromRange(range));
    NSLog(@"%@", [text attribute:NSStrikethroughStyleAttributeName atIndex:text.length - 1 effectiveRange:&range]);

    [returnValue addAttribute:NSStrikethroughStyleAttributeName value:@(NSUnderlineStyleSingle) range:NSMakeRange(text.length - 1, 1)];
    [returnValue addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(text.length - 1, 1)];
}
else {
    [returnValue addAttribute:NSStrikethroughStyleAttributeName value:@(NSUnderlineStyleSingle) range:NSMakeRange(range.location - 1, 1)];
    [returnValue addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(range.location - 1, 1)];
}

[returnValue removeAttribute:NSStrikethroughStyleAttributeName range:NSMakeRange(returnValue.length, 1)];

return returnValue;


}

However, no matter how hard I think, I can't wrap my head around the situation. This code doesn't work, or works partially. The value returned by attribute: atIndex: effectiveRange: is always nil, doesn't matter if the attribute actually exists or not. The effective range is out of bounds of the text I have.

Please help me out here.

Answer

Karl picture Karl · Jun 23, 2013

In your strikeText: method you're only checking the very end of your attributedString. If you want to check the last character you should check from text.length -2, assuming that text is long enough. Also you're removeAttribute in the end of the method does not make much sense too me.

A simple approach on how you can reuse the range from the Delegate-Protocol to strike only the characters you need:

- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
    // check if your replacement is going to be empty, therefor deleting
    if ([text length] == 0) {

        // don't strike spaces and newlines, could use NSCharacterSet here
        NSString *textToDelete = [textView.text substringWithRange:range];
        if (![@[ @" ", @"\n" ] containsObject:textToDelete]) {
            textView.attributedText = [self textByStrikingText:textView.attributedText inRange:range];
        }

        textView.selectedRange = NSMakeRange(range.location, 0);

        return NO;
    }

    return YES;
}

- (NSAttributedString *)textByStrikingText:(NSAttributedString *)text inRange:(NSRange)range
{    
    NSMutableAttributedString *strickenText = [[NSMutableAttributedString alloc] initWithAttributedString:text];

    [strickenText addAttribute:NSStrikethroughStyleAttributeName value:@(NSUnderlineStyleSingle) range:range];
    [strickenText addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:range];

    return strickenText;
}

There might be more edge cases but this is a simple approach that does what you want.