iosobjective-cnsprogressindicator

Objective C - Remove/Hide NSProgressIndicator after loop


I'm would like to update a progress bar using a thread, as explayned here. I'm trying to achieve this result:

  1. The progress bar becomes visible
  2. The progress bar is updated using the loop
  3. The progressbar disappears

This is my code:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
        
        progressBar.hidden = NO;

        for (NSInteger i = 1; i <= progressBar.maxValue; i += 20){

            
                [NSThread sleepForTimeInterval:1.0];
                dispatch_async(dispatch_get_main_queue(), ^{
                    [progressBar setDoubleValue:(double)i];
                    [progressBar displayIfNeeded];
                });
            }
        
        progressBar.hidden = YES;
        
    });

The progress bar is defined in my ViewController.h int this way:

NSProgressIndicator *progressBar

The problem is that the bar is not removed at the end of the loop, I don't know if progressBar.hidden = YES; works this way.

Can someone help me? A code snippet would be really useful, expecially if it's followed by an explanation.


Solution

  • What you're doing is not good for two reasons.

    First, sleeping a thread is not the right thing to do unless you own that thread or know exactly what responsibilities it has. The threads for the queues are owned by GCD, and you should keep your work at the level of the queues, not the lower level. (Blocks from the main queue will always run on the main thread. In certain, albeit limited, circumstances, a Block from a global queue may not be running on a background thread.*)

    Second, and the cause for the problem you asked about: being on a background thread, your setting of hidden is a UI operation on a non-main thread. This is not allowed, because it can cause synchronization problems in the UI's state. It's not safe to modify a view's appearance in Cocoa except from the main thread.

    You got that problem half right, dispatching to the main queue for the setDoubleValue: call, but setting hidden needs to be on the main thread as well.

    A for loop isn't a good mechanism for updating the screen. I'd suggest reworking your procedure to repeatedly call a method instead. An NSTimer is built for doing what you're doing. You should have no trouble finding an example of using one.

    If you'd like to use GCD, I'd suggest switching to use a single dispatch_after() call, repeatedly running a Block after a delay on the main queue. Something like this:

    - (void)kickItOff
    {
        self.progressBar.hidden = NO;
        [self updateProgress:0];
    }
    
    - (void)updateProgress:(double)progressValue
    {
        if( self.progressBar.maxValue <= progressValue ){
            self.progressBar.hidden = YES;
            return;
        }
    
        dispatch_time_t oneSecond = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC));
        dispatch_after(oneSecond, dispatch_get_main_queue(), ^{
    
            [self.progressBar setDoubleValue:progressValue];
            [self updateProgress:progressValue + 20];
        });
    }
    

    You start the update cycle by calling kickItOff. Then updateProgress: arranges the looping. This lets the main thread and run loop continue to work unimpeded, while ensuring that your code is running at the intervals you want.


    *More in depth on this point: in order for the UI to actually be drawn to the screen, the main run loop needs to be cycling. If the main thread is sleeping, that can't happen: the whole UI is locked up for both drawing and accepting input (and the main dispatch queue isn't being processed either).