views:

590

answers:

3

I have a segmented control being used as a toggle. When toggled, I switch a bunch of contents around in a table view which takes a tiny but noticeable amount of time (inserting/deleting sections in the table view, animating the change, etc). I want the segmented control to respond to the selection immediately. So in my action handling code for the segmented control's UIControlEventValueChanged event, I do the following:

- (IBAction)groupingChanged:(id)sender {
    UISegmentedControl *seg = sender;
    [tableModel toggleOn:[seg selectedSegmentIndex] == ToggleOnIndex];
    [self performSelectorOnMainThread:@selector(updateGrouping)
                           withObject:nil
                        waitUntilDone:NO];
}

Where updateGrouping is:

- (void)updateGrouping {
    MXAssertMainThread();

    [tableView beginUpdates];
    ... several table updates
    [tableView endUpdates];
}

Setting waitUntilDone:NO allows the groupingChanged method to complete before the updateGrouping is called, but this doesn't seem to be sufficient to repaint the view. The segmented control sticks until the table is done updating, then it switches over.

So I tried modifying groupingChanged: to create a thread for the update like so:

- (void)delayed {
    [self performSelectorOnMainThread:@selector(updateGrouping)
                           withObject:nil
                        waitUntilDone:NO];
}

- (IBAction)groupingChanged:(id)sender {
    UISegmentedControl *seg = sender;
    [tableModel toggleOn:[seg selectedSegmentIndex] == ToggleOnIndex];
    [self performSelectorInBackground:@selector(delayed) withObject:nil];
}

And this does work. The segmented control toggles instantly and the table soon follows. But I'm not at all confident of the result. Is it simply a side-effect of giving the main thread a reprieve while the new thread started up? Is this just how I need to queue updates to the UI? It's clearly hacky. I'm hoping someone has a better pattern they're following for this situation.

+1  A: 

If you just want to make sure the segmented control gets repainted real quick, I probably wouldn't dive into threads.

Instead I would just set a timer with a low value like 0.1, and that should be sufficient enough to get the control updated without any noticeable delay to the user.

I've used this when I have a lot of work to do but need a quick UI update.

Still a little "hacky", but without the introduction of threads.

So...

- (IBAction)groupingChanged:(id)sender {
    UISegmentedControl *seg = sender;
    [tableModel toggleOn:[seg selectedSegmentIndex] == ToggleOnIndex];
    [NSTimer scheduledTimerWithTimeInterval:0.1 
                                     target:self 
                                   selector:@selector(updateGrouping) 
                                   userInfo:nil 
                                    repeats:NO];    

}
Corey Floyd
That eliminates the interim thread, so it's an improvement, but I refuse to believe there's no common non-hacky pattern.
Nick Veys
performSelector:withObject:afterDelay is also valid as Kendall points out. This issue is that the run loop needs to complete to allow the UI to update. It's not really a hack, but just acknowledging this fact. There isn't anyway I know of to update the UI without allowing the main thread to run it's course.
Corey Floyd
Of course, I realize the cause. I guess I was hoping for a common pattern for this. Credit awarded (belated).
Nick Veys
I think this is the common pattern, I recommend Kendall's suggestion as the performSelector syntax is a little more natural. Also, by using this indirection in resolving the selector, you gain the flexibility of being able to pass an arbitrary selector to the method instead of a predetermined one. Slightly less code, a little more dynamic and reusable.
Corey Floyd
A: 

Think of it this way - everything you do is generally done on the main thread, including UI updates.

So in your original code, the code that updated the table view was reached before the code that did the UI update on the segment.

So, you were correct that giving the main thread a break allowed more time to complete the updates to the UI, because the main thread was allowed to complete the UI updates while the background thread handled the table updates.

An alternative you could try is to use performSelector:withObject:afterDelay with a 0.0 delay (which allows the main thread to process other things before proceeding with the selector). It may work where performSelectorOnMainThread did not, as that call may be more immediate even though they end up doing something very similar.

Kendall Helmstetter Gelner
A: 

I hit the same problem and solved it by subclassing the UISegmentedControl to create a Delayed UISegmentedControl.

In the delayed control I overrode addTarget:action:forControlEvents: to capture the target & action. Then when the segment event occurs I run an NSTimer to launch the captured target & action after a set delay. The result is that the UI gets updated to display the segment clicked and I can use the DelayedUISegmentedControl like I would a UISegmentedControl:

// Follows all normal initialization patterns of UISegmentedControl
UISegmentedControl *segmentedControl = [[DelayedUISegmentedControl alloc] 
    initWithItems:[NSArray arrayWithObjects: @"First", @"Second", nil]];

// Adds a delay to the selector; default is 0.25
[segmentedControl addTarget:self action:@selector(segmentAction:) 
    forControlEvents:UIControlEventValueChanged];

If you want to download the control I've open sourced it on google code.

Gavin Miller