Stackoverflow: “How to check whether a string contains white spaces”

Each day I am visiting stackoverflow I check my newly upvoted answers, and sometimes I visit an answer to see what I wrote.

Today I rediscovered the Question “How to check whether a string contains white spaces”. The question was how you know that a given NSString has whitespace.

This was my answer:

NSRange whiteSpaceRange = [foo rangeOfCharacterFromSet:[NSCharacterSet whitespaceCharacterSet]];
if (whiteSpaceRange.location != NSNotFound) {
    NSLog(@"Found whitespace");
}

This was the answer by utsabiem

NSArray *componentsSeparatedByWhiteSpace = [testString componentsSeparatedByString:@" "];
if([componentsSeparatedByWhiteSpace count] > 1){
    NSLog(@"Found whitespace");
}

Sounds legit. Since my answer doesn’t create an array it’s probably faster. But how much faster exactly?

To figure this out I wrote a simple test that I ran on an iPod Touch 4th Generation. This is the code I used:

	NSString *src = /* some very long text */

    NSInteger numberOfRuns = 1000;
    NSInteger maximumLengthOfSingleString = 10;
    
    NSInteger totalSourceLength = [src length];
    NSMutableArray *array = [NSMutableArray arrayWithCapacity:numberOfRuns];
    for (NSInteger i = 0; i < numberOfRuns; i++) {
        NSInteger startIndex = arc4random_uniform(totalSourceLength);
        NSInteger remainingLength = totalSourceLength - startIndex;
        NSInteger length = arc4random_uniform(MIN(maximumLengthOfSingleString, remainingLength));
        [array addObject:[src substringWithRange:NSMakeRange(startIndex, length)]];
    }
    NSDate *start;
    NSDate *stop;

    
    NSInteger hasWhiteSpace = 0;
    start = [NSDate date];
    for (NSString *str in array) {
        NSArray *componentsSeparatedByWhiteSpace = [str componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
        if ([componentsSeparatedByWhiteSpace count] > 1) {
            hasWhiteSpace++;
        }
    }
    stop = [NSDate date];
    NSLog(@"separate components (%d) duration %f", hasWhiteSpace, [stop timeIntervalSinceDate:start]);

    
    hasWhiteSpace = 0;
    start = [NSDate date];
    for (NSString *str in array) {
        NSRange whiteSpaceRange = [str rangeOfCharacterFromSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
        if (whiteSpaceRange.location != NSNotFound) {
            hasWhiteSpace++;
        }
    }
    stop = [NSDate date];
    NSLog(@"range of character from set (%d) duration %f", hasWhiteSpace, [stop timeIntervalSinceDate:start]);

The code first creates 1000 random strings (random start position, random length, maximum length is specified) from a large text. I used a 20 000 word long lorem ipsum created by lipsum.org.
Then it checks if the created strings have whitespace in them. I leveled the playing field by testing for both whitespace and newlines.

I did run this test with a max word length of 10, 100, 200 … 1000 characters.

Results:

max string length duration “separate components” (ms) duration “range of string” (ms)
10 20.06 2.465
50 46.925 2.54
100 72.144 2.789
200 132.629 3.002
300 192.899 3.147
400 257.483 3.195
500 310.304 3.347
600 386.354 3.458
700 459.688 3.505
800 481.396 3.555
900 549.651 3.302
1000 589.601 3.273

I made a chart to illustrate the difference.

Performance_Whitespace_Detection

Pretty big difference, huh?

Where does the huge difference come from?

I’m pretty sure rangeOfCharacterFromSet: stops after it found the first matching character. Whereas componentsSeparatedByCharactersInSet: has to separate the whole string. And the latter method creates a new array with many objects; which will also be much slower than returning a NSRange.

So what does that tell us?

You should know the frameworks. You know what they say about hammers and nails? If you only have a shallow grasp of the API available to you, you might have a powerful hammer and get things done; but you might be a hundred times slower than the guy with the wrench.

And don’t hesitate to write a quick test. Be curious about the code you copy from somewhere, it only took 40 lines of code to make those tests. If you have bad feelings about code you see somewhere, write a test and prove or invalidate your feelings.

Posted in Coding Tagged with: , ,

a color indicator in a tableView cell

Today I created a static tableview which should allow the user to change colors of items.

To indicate the selected color I put a UIImageView (to display a horizontal red bar if no color is used) in the cell, and set its backgroundColor property.

Because this is a static tableView I set the backgroundColor of my color indicator views in a method called - configureView. Nothing fancy.

- (void)configureView {
    for (UIImageView *colorView in self.colorViews) {
        colorView.backgroundColor = [self colorForView:colorView];
    }
}

After I implemented everything I took it for a test ride.

tableView before pushing

Works as intended.

If you select the row a segue will push a color picker view controller, where you can select a new color.

color selection viewController

So I set the color and tapped the back button

tableView during popping

Everything is fine.

But moments later…

tableView after popping

The color is reset to the value before we changed it.

Let’s debug it

So I started to check what I did wrong. Maybe I didn’t set the selected color. Nope, I did that. I used the debugger to step over each line of code that happened after I pressed the back button.

But I couldn’t figure out what I did wrong.

To figure out which method changed the backgroundColor I subclassed the color indicator view and changed - setBackgroundColor: to print the stack trace.

- (void)setBackgroundColor:(UIColor *)backgroundColor {
    [super setBackgroundColor:backgroundColor];
    NSLog(@"%@", [NSThread callStackSymbols]);
}

I went back to the color picker and cleared the log.

The first stack trace looked good. Exactly like it should. The view will appear, and configureView changes the backgroundColor

0  MyApp  0x00034ddf -[MBImageView setBackgroundColor:] + 143
1  MyApp  0x00027bef -[MBColorListTableViewController configureView] + 639
2  MyApp  0x00027303 -[MBColorListTableViewController viewWillAppear:] + 115

But. The backgroundColor property is changed more often…

0  MyApp  0x00034ddf -[MBImageView setBackgroundColor:] + 143
1  UIKit  0x0053bd92 -[UITableViewCell _setOpaque:forSubview:] + 896
2  UIKit  0x0053bed9 -[UITableViewCell _setOpaque:forSubview:] + 1223
3  UIKit  0x0053bed9 -[UITableViewCell _setOpaque:forSubview:] + 1223
4  UIKit  0x0053cfae -[UITableViewCell _updateHighlightColors] + 198
5  UIKit  0x0053cb43 -[UITableViewCell _deselectAnimationFinished] + 178
6  UIKit  0x00387d66 -[UIViewAnimationState sendDelegateAnimationDidStop:finished:] + 237
7  UIKit  0x00387f04 -[UIViewAnimationState animationDidStop:finished:] + 68

There it is. After I changed the backgroundColor, the cell changed the backgroundColor too.

And it appears it changes the color back to the previous value.

And if I hadn’t cleared the log without looking at it, I would have seen that the same thing happened before

0  MyApp  0x00034de3 -[MBImageView setBackgroundColor:] + 115
1  UIKit  0x0053bd92 -[UITableViewCell _setOpaque:forSubview:] + 896
2  UIKit  0x0053bed9 -[UITableViewCell _setOpaque:forSubview:] + 1223
3  UIKit  0x0053bed9 -[UITableViewCell _setOpaque:forSubview:] + 1223
4  UIKit  0x0053cfae -[UITableViewCell _updateHighlightColors] + 198
5  UIKit  0x0053c8b3 -[UITableViewCell showSelectedBackgroundView:animated:] + 1455
6  UIKit  0x0053c9ed -[UITableViewCell setHighlighted:animated:] + 224

So why does this happen?

It turns out when you select a cell by tapping it, the cell goes through all its subviews and sets the backgroundColor to UIDeviceWhiteColorSpace 0 0 also known as [UIColor clearColor]. And when you deselect the cell, it goes again through all its subviews and sets the backgroundColor back to their previous (before it was selected) value.

This is what happens to the backgroundColor property:

  1. The tableView is presented
    backgroundColor -> gray

  2. I select the row, selection animation does start
    backgroundColor -> clear

  3. The next viewController is pushed, there I change the color and pop back to the table
    backgroundColor -> green

  4. The tableview appears and starts and finishes the deselection animation.
    backgroundColor -> gray

This is done so the selectedBackground is visible through all the UIViews in the cell (which for performance reasons should have a white backgroundColor, instead of a clear one).

Conclusion

Obviously you can’t change the backgroundColor of a UITableViewCell subview after the selection animation started and before the deselection animation has come to an end.

As far as I know that behavior is not documented anywhere.

Solution

You could save the proposed backgroundColor as a property on the viewController and set it after the animation is completed. You could probably kill the animation completely too.

But I went for a better (as in cleaner, easier and better looking) method. I simply added a CALayer to the UIImageView in viewDidLoad

- (void)viewDidLoad
{
    [super viewDidLoad];
    for (UIImageView *colorView in self.colorViews) {
        CALayer *colorLayer = [CALayer layer];
        colorLayer.frame = CGRectMake(0, 0, colorView.bounds.size.width, colorView.bounds.size.height);
        [colorView.layer addSublayer:colorLayer];
    }
}

And I changed my configureView method so it adjusts the backgroundColor of that layer

- (void)configureView {
    for (UIImageView *colorView in self.colorViews) {
        CALayer *colorLayer = colorView.layer.sublayers[0];
        colorLayer.backgroundColor = [self colorForView:colorView].CGColor;
    }
}

Works great.

Posted in Coding Tagged with: , ,