user1419810
user1419810

Reputation: 846

Efficient find and replace text in Objective C

I'm currently trying to find an replace text strings as a user types text into a tool bar. I've done this so far by updating the text view with the ViewDidChange method and using stringByReplacingOccurrencesOfString as shown in the code below.

This works well when finding and replacing a few (10-20) strings however when doing this with 1,000 + potential replacements the system become extremely laggy - you can see the drop in performance in the simulator as you type.

Does anyone know how to make this more efficient? Is this because of a memory leak? One idea I've had is only to only use the replace string method on the last word in the string (i.e. only look at the last word the user has typed however that is just a guess at how this might speed things up, any other thoughts/ideas/hits would be greatly appreciated!!

Another point to note, if replacing text with anything unicode based the system slows down even more, does anyone know of a specific way to fix this?

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

    textView.text =  [textView.text stringByReplacingOccurrencesOfString:@"(?wi)\\bsmart\\b" withString: @"clever" options: NSRegularExpressionSearch range: NSMakeRange(0, [textView.text length]) ];
    textView.text =  [textView.text stringByReplacingOccurrencesOfString:@"(?wi)\\bfast\\b" withString: @"speedy" options: NSRegularExpressionSearch range: NSMakeRange(0, [textView.text length]) ];
    textView.text =  [textView.text stringByReplacingOccurrencesOfString:@"(?wi)\\bhappy\\b" withString: @"content" options: NSRegularExpressionSearch range: NSMakeRange(0, [textView.text length]) ];         
}

Upvotes: 0

Views: 284

Answers (3)

Currently, you're doing multiple passes along the string, one for each word-to-be-replaced. You don't need this; simply go through the string once, replacing words on the fly, thereby saving memory allocations.

You could also pre-compile one (huge) regular expression and store it in an instance variable in order to avoid frequent recompilation thereof.

You could also move the options out of the non-capturing group and specify them as global flags (I don't know how good NSRegularExpression's optimizer is at detecting and hoisting superfluously repeated flags, but regex optimizers are traditionally not very smart – if any.)

// ivars
NSRegularExpression *regEx;
NSDictionary *replacementRules;

- (instancetype)init {
    ...
    replacementRules = @[
        @"smart": @"clever",
        @"fast":  @"speedy",
        @"happy": @"content"
    ];

    // 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];
    ...
}

- (void)textViewDidChange:(UITextView *)textView {
    ...

    // 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;
}

Upvotes: 1

RegularExpression
RegularExpression

Reputation: 3541

I can't think of a better application for regular expressions.

Please see: NSRegularExpression, and this article:

http://www.raywenderlich.com/30288/nsregularexpression-tutorial-and-cheat-sheet

Upvotes: 0

Caleb
Caleb

Reputation: 125007

however when doing this with 1,000 + potential replacements the system become extremely laggy

That's not surprising. Notice that -stringByReplacingOccurancesOfString: creates an entirely new copy of the target. Doing that 1000+ times even on a small string is going to take a little time. Alternatively, doing that even a few times on a string that's large enough to contain 1000+ substrings is going to take some time.

The first step in any performance improvement effort should be to measure the actual performance you're getting now. You can use Instruments for that. Run the same test a few times and get a baseline for the time needed to do whatever task you're doing. Then start making changes and measure each one to see where you get a real improvement.

When you're ready to start making changes, I'd try switching to using a single mutable string and a method like -replaceOccurrencesOfString:withString:options:range: instead of your current approach. That should at least eliminate most of the copying you're doing now.

Upvotes: 0

Related Questions