NSProgress was introduced in iOS 7 and OS X 10.9 with the self-proclaimed purpose of being a loosely coupled progress reporting mechanism. In practice this means that intermediary code doesn’t need to know anything about the progress being tracked between the user interface layer where your application is doing work.
Let’s see how this works in practice. In our view controller we can setup something like this:
self.progress = [NSProgress progressWithTotalUnitCount:-1];
[self.progress becomeCurrentWithPendingUnitCount:1];
[APIClient doSomethingWithCompletion:^{
// Asynchronous activity is done
}];
[self.progress resignCurrent];
Here we are creating a new NSProgress instance on the main thread.
Setting the totalUnitCount to -1 means our progress starts with an
indeterminate state. In our UI we would reflect this state with a
UIActivityIndicatorView on iOS or a NSProgressIndicator with
indeterminate set to YES on OS X.
Then we tell our NSProgress to become the current progress. Under the
hood this registers our newly created progress as the +currentProgress
for this thread. This becomes important when we want to create child
NSProgress instances elsewhere in your code which will affect this
main progress. Now we can perform some asynchronous work, and resign
from the current progress. Resigning the progress restores the previous
NSProgress instance where becomeCurrentWithPendingUnitCount: was
called. This allows us to chain multiple progress objects that are all
doing work on the same thread.
Now, in our doSomethingWithCompletion: implementation, no matter how
many levels deep it is, we can grab the NSProgress object we just
created and build off of it without explicitly passing it as an
argument. Let’s look at how this may work:
- (void)doSomethingWithCompletion:(dispatch_block_t)completionBlock
{
// Our progress instance from above
NSProgress *mainProgress = [NSProgress currentProgress];
// A new child progress
NSProgress *progress = [NSProgress progressWithTotalUnitCount:-1];
[self getObjectIDsWithCompletion:^(NSArray *IDs) {
dispatch_async(dispatch_get_main_queue(), ^{
mainProgress.totalUnitCount = 1;
progress.totalUnitCount = IDs.count;
});
[self getEachObjectFromIDs:IDs
withBlock:^(NSDictionary *object) {
[self processObject:object];
dispatch_async(dispatch_get_main_queue(), ^{
progress.completedUnitCount++;
});
} completion:^{
dispatch_async(dispatch_get_main_queue(), ^{
progress.completedUnitCount = progress.totalUnitCount;
completionBlock();
});
}];
}];
}
There is a lot going on here so let’s step through it. We first grab the
mainProgress we created in our UI layer. We only need to do this
since, in this example, our progress starts off indeterminate until our
first network request has completed. We then create a new child progress
that is also indeterminate to start. Under the hood this is calling
-initWithParent:userInfo: on NSProgress and passing the
+currentProgress which happens to be our mainProgress from the UI
layer. Because of the loose coupling here, if the UI layer wasn’t
utilizing this NSProgress chain, our networking code could look
exactly the same. The only difference is no one would be observing the
changes that are happening.
Next we fire off our first asynchronous call which gets us an array of
IDs of objects that we need to fetch. Now that we know how many
requests we are going to perform our NSProgress instances no longer
need to be indeterminate. Our mainProgress gets a totalUnitCount of
1, since the only unit of work that needs to be completed is that being
completed by our single child progress. As for the child progress, it
gets a totalUnitCount matching the number of requests we need to
perform. Each time one of these requests finishes, we increment the
completedUnitCount which is automatically reflected by our
mainProgress. We will look at binding this to our UI momentarily.
Finally, we call our completion block which brings us back up to the UI
layer.
At this point we have a fully functional NSProgress chain that we can
observe changes on and reflect in our UI.
We have many options on how to bind our NSProgress instance to our UI.
On OS X we could use Cocoa Bindings. Otherwise we could use KVO,
possible with a wrapper or ReactiveCocoa. On OS X we can initialize
our NSProgressIndicator with a minValue and maxValue of 0.0 and
1.0 respectively. This will match the behavior of UIProgressView
so that we can bind either value on NSProgressIndicator or
progress on UIProgressView to the fractionCompleted on
NSProgress. The fractionCompleted property uses all child progress
instances to compute an overall value for the fraction that is completed
between instances. This makes for a really accurate representation of
overall progress for multiple operations.
After setting this up we now have an awesome looking and more informative progress indicator. Here is the indicator we’ve created:
