Dov Dov - 26 days ago 8
Objective-C Question

How to use/test NSProgress userInfo changes of a child NSProgress instance

I'm implementing

NSProgress
support in a library, and I wrote some unit tests to test that everything's working correctly. While ideally I'd like to be able to pass some additional metadata (
userInfo
keys not used by
NSProgress
itself, but for users of my API to consume), for now I'm just trying to get
localizedDescription
and
localizedAdditionalDescription
to work like the documentation says they should. Since the method I'm testing extracts files from an archive, I set the
kind
to
NSProgressKindFile
and set the various keys associated with file operations (e.g.
NSProgressFileCompletedCountKey
).

I expect when I observe changes to
localizedDescription
with KVO, that I'll see updates like this:


Processing “Test File A.txt”

Processing “Test File B.jpg”

Processing “Test File C.m4a”


When I stop at a breakpoint and
po
the
localizedDescription
on the worker
NSProgress
instance (
childProgress
below), that is in fact what I see. But when my tests run, all they see is the following, implying it's not seeing any of the
userInfo
keys I set:


0% completed

0% completed

53% completed

100% completed

100% completed


It looks like the
userInfo
keys I set on a child
NSProgress
instance are not getting passed on to its parent, even though
fractionCompleted
does. Am I doing something wrong?


I give some abstract code snippets below, but you can also download the commit with these changes from GitHub. If you'd like to reproduce this behavior, run the
-[ProgressReportingTests testProgressReporting_ExtractFiles_Description]
and
-[ProgressReportingTests testProgressReporting_ExtractFiles_AdditionalDescription]
test cases.

In my test case class:

static void *ProgressContext = &ProgressContext;

...

- (void)testProgressReporting {
NSProgress *parentProgress = [NSProgress progressWithTotalUnitCount:1];
[parentProgress becomeCurrentWithPendingUnitCount:1];

[parentProgress addObserver:self
forKeyPath:NSStringFromSelector(@selector(localizedDescription))
options:NSKeyValueObservingOptionInitial
context:ProgressContext];

MyAPIClass *apiObject = // initialize
[apiObject doLongRunningThing];

[parentProgress resignCurrent];
[parentProgress removeObserver:self
forKeyPath:NSStringFromSelector(@selector(localizedDescription))];
}


- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context
{
if (context == ProgressContext) {
// Should refer to parentProgress from above
NSProgress *notificationProgress = object;

[self.descriptionArray addObject:notificationProgress.localizedDescription];
}
}


Then, in my class under test:

- (void) doLongRunningThing {
...
NSProgress *childProgress = [NSProgress progressWithTotalUnitCount:/* bytes calculated above */];
progress.kind = NSProgressKindFile;
[childProgress setUserInfoObject:@0
forKey:NSProgressFileCompletedCountKey];
[childProgress setUserInfoObject:@(/*array count from above*/)
forKey:NSProgressFileTotalCountKey];

int counter = 0;

for /* Long-running loop */ {
[childProgress setUserInfoObject: // a file URL
forKey:NSProgressFileURLKey];

// Do stuff

[childProgress setUserInfoObject:@(++counter)
forKey:NSProgressFileCompletedCountKey];
childProgress.completedUnitCount += myIncrement;
}
}


At the time I increment
childProgress.completedUnitCount
, this is what the userInfo looks like in the debugger. The fields I set are all represented:

> po childProgress.userInfo

{
NSProgressFileCompletedCountKey = 2,
NSProgressFileTotalCountKey = 3,
NSProgressFileURLKey = "file:///...Test%20File%20B.jpg"; // chunk elided from URL
}


When each KVO notification comes back, this is how
notificationProgress.userInfo
looks:

> po notificationProgress.userInfo

{
}

Answer Source

Ok, I had a chance to look at the code again with more coffee in my system and more time on my hands. I'm actually seeing it working.

In your testProgressReporting_ExtractFiles_AdditionalDescription method, I changed the code to this:

NSProgress *extractFilesProgress = [NSProgress progressWithTotalUnitCount:1];
[extractFilesProgress setUserInfoObject:@10 forKey:NSProgressEstimatedTimeRemainingKey];
[extractFilesProgress setUserInfoObject:@"Test" forKey:@"TestKey"];

And then in observeValueForKeyPath, I printed these objects:

po progress.userInfo {
NSProgressEstimatedTimeRemainingKey = 10;
TestKey = Test;
}

po progress.localizedAdditionalDescription
0 of 1 — About 10 seconds remaining

You can see the key-values I added, and the localizedAdditionalDescription was created based on those entries (notice the time remaining). So, this all looks like it's working correctly.

I think one point of confusion might be around the NSProgress properties and their effect on the key-values in the userInfo dict. Setting the properties doesn't add key-values to the userInfo dict, and setting the key-values doesn't set the properties. For example, setting the progress kind doesn't add the NSProgressFileOperationKindKey to the userInfo dict. The value in the userInfo dict, if present, is more of an override of the property that's only used when creating the localizedAdditionalDescription.

You can also see the custom key-value I added. So, this all looks like it's working right. Can you point me to something that still looks off?