user1419810
user1419810

Reputation: 846

iOS ObjectiveC find and replace/regex

I'm using the following to find and replace text as a user types into a textView

- (void)textViewDidChange:(UITextView *)textView {
    if (textView != self.inputToolbar.contentView.textView) {
        return;
    }

    NSRegularExpression *regEx;
    NSDictionary *replacementRules = @{
                                       @"cheese ": @"ham ",
                                       @"cat ": @"dog ",
                                       @"smile ": @"grin ",
                                       };

    // Build regular expression
    NSMutableArray *patterns = [NSMutableArray arrayWithCapacity:
                                replacementRules.count];

    for (NSString *str in replacementRules.allKeys) {
        [patterns addObject:[NSString stringWithFormat:@"\\b(%@)\\b", str]];
    }

    NSString *reStr = [patterns componentsJoinedByString:@"|"];

    regEx = [NSRegularExpression
             regularExpressionWithPattern:reStr
             options:NSRegularExpressionUseUnicodeWordBoundaries | NSRegularExpressionCaseInsensitive
             error:NULL];

    // our new string
    NSMutableString *s = [NSMutableString new];
    NSUInteger __block lastPos = 0;

    [regEx enumerateMatchesInString:textView.text
                            options:kNilOptions
                              range:(NSRange){ 0, textView.text.length }
                         usingBlock:^(NSTextCheckingResult *result,
                                      NSMatchingFlags flags,
                                      BOOL *stop) {

                             // Append the string from _before_ the match
                             [s appendString:[textView.text substringWithRange:(NSRange){
                                 lastPos, result.range.location - lastPos
                             }]];
                             lastPos = result.range.location + result.range.length;

                             // actually replace the string
                             NSString *captured = [textView.text substringWithRange:result.range];
                             [s appendString:replacementRules[captured]];
                         }];

    // append rest of string, from after the last match
    [s appendString:[textView.text substringWithRange:(NSRange){
        lastPos, textView.text.length - lastPos
    }]];

    textView.text = s;
}

This works fine for strings of less than 8 or so words however as the string of words gets longer typing slows down considerably and becomes laggy (whether a find and replace of text is executed or not.) This massively impacts user experience, does anyone know why this might be or how to fix it?

Upvotes: 1

Views: 126

Answers (2)

Peter Foti
Peter Foti

Reputation: 5654

I'm not certain you need to use regex so this solution might work a bit better if in fact you do not. It gets the last word entered before a space is pressed, if that word matches anything in our dictionary we replace it, if not we continue.

#import "ViewController.h"

@interface ViewController () <UITextViewDelegate>

@property (weak, nonatomic) IBOutlet UITextView *textView;
@property (strong, nonatomic) NSDictionary *words;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.words = @{ @"word": @"replace_word",
                    @"peter": @"replace_peter" };
}

- (void)textViewDidChange:(UITextView *)textView
{
    NSString *lastCharacter = [textView.text substringWithRange:NSMakeRange([textView.text length]-1, 1)];
    if([lastCharacter isEqualToString:@" "]) {
        NSMutableArray *words = (NSMutableArray *)[textView.text componentsSeparatedByString:@" "];
        NSString *lastWord = words[words.count - 2];
        for (NSString *word in self.words.allKeys) {
            if ([lastWord isEqualToString:word]) {
                lastWord = self.words[word];
                [words replaceObjectAtIndex:words.count - 2 withObject:lastWord];
                textView.text = [words componentsJoinedByString:@" "];
                break;
            }
        }
    }
}

@end

Upvotes: 0

Sergey Kalinichenko
Sergey Kalinichenko

Reputation: 726579

There are several things that you could do to improve the speed.

First, you need to move initialization of the replacementRules dictionary and the corresponding regex away from the textViewDidChange method. Constructing a dictionary and a regex every time the end-user types or deletes a character is rather expensive.

Next, you should change the regex to reduce the number of capturing groups. Currently, your regex looks like this:

\\b(cheese)\\b|\\b(cat)\\b|\\b(smile)\\b

You can change to the equivalent regex

\\b(cheese|cat|smile)\\b

to make the job of regex engine easier.

Finally, you should make a flag indicating that a replacement has happened, and set it from inside the block. If the flag is not set by the time you exit out of enumerateMatchesInString method, you can return right away, and skip the [s appendString...] part altogether.

Upvotes: 2

Related Questions