how to animate progress indicator / level indicator?

I can't seem to find any good information on how one animates a progress indicator (or level indicator). I have created a test project with a progress indicator and a button called "run."



@interface RootViewController ()

@property (weak) IBOutlet NSProgressIndicator *progressIndicator;


@end


@implementation RootViewController


- (IBAction)runButton:(NSButton *)sender

{

int i;

for (i = 0; i < 20; i++)

{

[_progressIndicator incrementBy:5.0];

[NSThread sleepForTimeInterval:(1)];

}

}



Nothing happens for 20 seconds, and then boom, the indicator is up at max. How can I get the bar to update while the run button method is being called. KVO? some cocoa binding? NSAnimation? Please send me down the right rabbit hole... ;]


Thanks!

Accepted Reply

Reason is that "When you set the value of the progress indicator, the OS X display mechanism won't actually draw the difference until the next time through the event loop, which doesn't happen until after your method returns. In other words, you're setting it all the way to 10 before the progress indicator even gets a chance to redraw itself, so all you see is the final filled state."


Here is the rabbit hole (Swift, but should be easy in objC).

https://stackoverflow.com/questions/38212874/swift-how-to-use-nsprogressindicator

Replies

Reason is that "When you set the value of the progress indicator, the OS X display mechanism won't actually draw the difference until the next time through the event loop, which doesn't happen until after your method returns. In other words, you're setting it all the way to 10 before the progress indicator even gets a chance to redraw itself, so all you see is the final filled state."


Here is the rabbit hole (Swift, but should be easy in objC).

https://stackoverflow.com/questions/38212874/swift-how-to-use-nsprogressindicator

Never do loops, and especially sleeps, in an interface builder action. If you want to simulate something, use a background thread or timer.

So question... in that stackoverflow example... granted Swift is like greek to me, but the only difference I can see is that the method is in the app delegate and his delay function lies outside the method. He is saying the progressIndicator won't be able to update until the action method finishes, but then he still has a for loop and a delay like I had. If it's true that the indicator won't update while the action method is running, then how is his example any different, other than the outside delay function?


I see the word dispatch in his delay function, which makes me think that the solution really involves concurrent programming. Am I correct? I'm starting to read the Concurrency Programming Guide... is that the right track?


UPDATE: I found this, which is pretty close to what I'm after...


https://stackoverflow.com/questions/7290931/gcd-threads-program-flow-and-ui-updating


BUT! he says ... "you cannot update the UI from a background thread." Ugh! Stab me in the heart. So how do progress bars even work then? If you have some process that is going to take a while, can that process not update the UI somehow? I feel like I'm the only person that has ever wanted to have their app have a progress bar by the way.


Let me give a more concrete example of what I'm after, maybe I'm just not framing this right. Lets say I have an app that can find prime numbers between any two numbers. So I, the user, enter in 100 billion and 110 billion. This will take the algorithm some time to crunch. So, rather than just sit there and look like the app has crashed, I want for that method to increment a textField on the UI by one every time it finds a prime number. This can be done?

gotcha... I'll learn about NSTimer. Thanks!

OMG. I got it to work. Using a determinate progress bar even. I feel like I just invented the light bulb.


.xib has determinate progress bar with min: 0 max: 100... and a run button and status label.



#import "RootViewController.h"


@interface RootViewController ()

@property (weak) IBOutlet NSProgressIndicator *progressIndicator;

@property (weak) IBOutlet NSTextField *statusLabel;

@property (weak) IBOutlet NSButton *runButtonOutlet;

@end



@implementation RootViewController


- (IBAction)runButton:(NSButton *)sender

{

[self.statusLabel setStringValue:@"Working ..."];

[self.runButtonOutlet setEnabled:NO];

[_progressIndicator startAnimation:nil];



dispatch_queue_t queue = dispatch_queue_create("com.sample", 0);

dispatch_queue_t main = dispatch_get_main_queue();


dispatch_async(queue,

^{

[self performLongRunningWork];

dispatch_async(main, ^{ [self workDone]; });

});


}


- (void)performLongRunningWork //finds large prime numbers between two numbers, takes about 15 seconds.

{

long i, j, flag, temp = 0;

int count = 0;

long num1 = 20000000;

long num2 = 20001000;

temp = num1;

if ( num1 % 2 == 0)

{

num1++;

}

for (i = num1; i <= num2; i = i + 2)

{

flag = 0;

for (j = 2; j <= i / 2; j++)

{

if ((i % j) == 0)

{

flag = 1;

break;

}

}

if (flag == 0)

{

printf("%ld\n", i);

count++;

dispatch_async(dispatch_get_main_queue(), ^{

[self.progressIndicator incrementBy:1.00];

});

}

}

printf("Number of primes between %ld & %ld = %d\n", temp, num2, count);

}


- (void)workDone

{

[self.statusLabel setStringValue:@"Done ..."];

[self.runButtonOutlet setEnabled:YES];

[_progressIndicator stopAnimation:nil];

}

No, the for loop and the delay in that Stack Overflow post is nothing like yours. That is a problem with Stack Overflow. If you just blindly copy and paste from it, you are only going to cause problems for yourself. In that example, the for loop is in "applicationDidFinishLaunching". This does happen on the main queue, but not in response to a UI action. More importantly, you were using a sleep operation. The Stack Overflow example is using a delay - radically different. That delay schedules a block to execute on the current (main) thread after a delay. So the for loop just schedules a bunch of quick operations to run at fixed points in the future, on the main thread. But the main thread is not being blocked as it is in your code.


In your new example, you are not really using threads. You are using GCD via queues. Queues are typically implemented as threads, but they are a different concept. When you called "dispatch_queue_create", you are creating a serial queue. Then you call dispatch_async on it. I'm not sure what happens in that case, but it probably isn't what you want. The documentation for "dispatch_queue_create" clearly explains how to use it and what that second parameter is for. If there are macros, use them. That would probably have alerted you that something was amiss.


You seem to be handling the "UI operations on the main thread" issue correctly. You are dispatching blocks asynchronously onto the main thread. Generally that will work. But you have to kick off your long-running process in a background thread. You can use the "dispatch_get_global_queue" function to obtain a reference to a global concurrent queue. You can then call "dispatch_async" on that to run something in the background. Then, from that background thread, you can dispatch blocks onto the main queue to update the UI.


A background thread is just one way to update a progress indicator. You can also use timers, "delays", or any other method that is appropriate. The key part is that UI actions need to run quickly. No sleeps. No loops. You can kick off async tasks back onto the main thread. This is required in some cases. But the same rules apply. That is a task on the main queue. You can't dawdle. If you need a long-running task, use concurrency in some fashion.

Awesome, thank you so much. I'm still trying to wrap my brain around when you would want to use serial queues vs. global queues. My understanding so far is that serial queues will run in FIFO order, and global cues may, or may not run in that order as they are allowed to harness the abilities of a mulit-core system. For me right now, just wanting my UI to update while some number crunching occurs, the serial cue feels safer.

It is not a safety question. Serial queues are required for some operations and concurrent queues are required for others. You can‘t mix and match.

But it is okay to iterate in a concurrent thread, yes?

It is best not to mix and max terminology. A "background thread" is a generic concept can be implemented in a number of different ways. The key part is that it is in the background, not the main user interface, and that it is a thread, which means it belongs to the running process. (Technically you can do it with a different process too.) Otherwise, you get caught up in confusion like your other thread. People think you are implementing a generic "background thread" using a very specific construct for a very specific reason. Technically you can do that, but you get trapped in terminology and subtleties.


The main user interface thread is special. This is the context where all of these action methods (identified by IBAction) and framework methods (like applicationDidFinishLaunching) are executed. You have to be careful with the main user interface thread. This is what makes software development for a graphical user interface more difficult than simple command-line tools.


Outside of the main user interface thread, you can do what you want, as long as you are careful if you ever need to touch the main user interface thread, you know, because it is special. Beyond that, if you get into true "concurrency", then you also have to be careful with any data that might be shared between threads, even if the main user interface thread is not involved. (Note: queues and threads are different, except for the main user interface there there is only one main queue and it is the main thread.) Going even further afield, Apple is heavily pushing heavily asynchronous code, where virtually everything gets dispatched onto some concurrent (or serial) queue to be executed "later". I strongly urge caution when going down this rabbit hole.


For now, if you want to simulate, or perform, some long running operation, dispatch it onto the global concurrent queue. If you need to touch the main thread, you can then dispatch it back onto the main queue, or maybe even use a notification. The main thread can be very funky sometimes. dispatch_async will not work 100% of the time. And then sometimes, you must do a dispatch_async even when you are already in the main thread.

Man, this is hard. Okay, if I have a long running process, but the calling method is dependant on what it returns before moving on to other tasks... I should a). still use a global queue b). use a serial cue or c). use something like a semaphore that will wait for the necessary data?


Thanks as always!