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: