Learn Objective-C on the Mac - Mark Dalrymple [156]
Figure 16-2. Adding a progress indicator
With the progress indicator selected, use the Attributes Inspector to make sure that its Display When Stopped checkbox is turned off. Then switch to the Bindings Inspector, and configure a binding for its Animate attribute, binding to the app delegate with its isWorking key. Then select the Start button in your window, where we’ll make a similar configuration. Bind the button’s Enabled attribute to the app delegate’s isWorking key, and this time add an NSNegateBoolean for the Value Transformer.
Now, we’re even more done with this. Save your changes, Build & Run, and you’ll now see that when you click the Start button, it becomes disabled, and the circular progress indicator appears and starts spinning. When the work is done, the progress indicator disappears, and the button goes back to normal.
Now let’s take this just one step farther, and add a horizontal progress indicator, where a horizontal movement tells you that things are happening, such as you may see in a software installer. Our progress view will go from 0 to 4, each work-method upping the number a bit. Like the circular progress indicator, this will be configured entirely with Cocoa Bindings.
Start off by adding a new instance variable and property called completed to our app delegate’s .h and .m files:
Next, add the following near the top of the doWork: method, to reset it as soon as the user hits the Start button:
Each work-method now needs to increment this variable when they’re done. Our first thought might be to add the following line to each work-method:
However, this is where one of those sticky multi-threaded problems turns up. If you think about that line of code, you’ll see that what really happens that it first grabs the current value of the completed property by calling [self completed], then adds one to it, and then stores the result back into the completed property by calling [self setCompleted:]. In a multi-threaded environment, this can lead to incorrect behavior. What if, for instance, two threads execute a line like this at about the same time? Suppose the starting value of completed is 2. If both threads read the current value before either of the writes out their own result, each will add 1 to their own copy of the value, then each of them will write their local sum (3) back into the completed property, which ends up containing the value 3 instead of the correct value, 4.
A way around this is to use Objective-C’s @synchronized keyword, which lets us specify that a piece of code can only be run by one thread at a time. Enter the following method at the top of your app delegate’s @implementation section:
What we’re doing there is taking the same code we thought about earlier and wrapping it in a sort of a safe zone. The @synchronized keyword takes as its one argument an object which will be used to determine the scope of the synchronization limiting. Basically, any calls using the same value will each try to grab the same lock. If some other thread has already claimed it, the current thread has to wait until the other thread is done. In this case, because we’re using self, any calls made to this method on the same instance will all try to acquire the same lock. We only have one instance of our app delegate, so this is in practice a global lock, but for our purposes that’s okay.
The coding we need to do in order for this to work is to just add this line to the end of each of the work-methods:
And finally, we need to configure the GUI. Go back to the MainMenu.xib file in Interface Builder, and search the Library for