Is it possible to dynamically extend an NSProgress hierarchy after progress has started?
Asked Answered
M

2

7

Say I have some hierarchy of NSProgress objects. For simplicity say a root progress with 2 children. If progress has already started can I add another child progress to the hierarchy and expect the right thing to happen? For example, say I start with 2 children, so the totalUnitCount of the root is 2 and the pendingUnitCount is 2. Once the first child is complete the root progress' fractionCompleted will be 0.5. If I then add another child progress the root progress' fractionCompleted does update to ~0.33 as I would like but the root's totalUnitCount remains at 2, as does the pendingUnitCount. I can update the totalUnitCount manually but this throws the root's fractionCompleted off. Also, there seems to be no way to update the pendingUnitCount without calling becomeCurrentWithPendingUnitCount:. Was NSProgress not designed for this kind of use or is there a "proper" way to do this?

My use case is that I have an application that can upload files to a remote server. The user may select some files and hit a button to start uploading them. While they are uploading the user sees the overall progress of the upload job. Now, if the user wants to select some more photos to upload while the uploads are already in progress I want to update the progress to reflect the new total. For example, he is currently uploading 2 files, 1 has already completed, while they are uploading he adds 2 more to upload. The progress should adjust itself. Total unit count should now be 4 and there should be 3 items remaining to upload. Seems like this would be a common use case to me.

Here's a unit test I wrote to play with dynamically extending an NSProgress hierarchy. All the asserts in the test below pass but if you check the root's totalUnitCount it always remains 2 and there seems to be no way to tell how many items remain to be completed from the root progress. The root progress is the one that the UI would observe so it is important that changes deeper in the hierarchy propagate up, including totalUnitCount, etc.

- (void)testDynamicNSProgress
{
    NSProgress *root = [NSProgress progressWithTotalUnitCount:2];
    [root becomeCurrentWithPendingUnitCount:2];

    NSProgress *child1 = [NSProgress progressWithTotalUnitCount:1];
    NSProgress *child2 = [NSProgress progressWithTotalUnitCount:1];
    XCTAssertEqual(root.totalUnitCount, 2);
    XCTAssertEqual(root.completedUnitCount, 0);

    child1.completedUnitCount++;
    XCTAssertEqual(ceil(root.fractionCompleted * 100), 50);

    NSProgress *child3 = [NSProgress progressWithTotalUnitCount:1];
    NSProgress *child4 = [NSProgress progressWithTotalUnitCount:1];
    XCTAssertEqual(ceil(root.fractionCompleted * 100), 25);

    child2.completedUnitCount++;
    XCTAssertEqual(ceil(root.fractionCompleted * 100), 50);

    child3.completedUnitCount++;
    XCTAssertEqual(ceil(root.fractionCompleted * 100), 75);

    child4.completedUnitCount++;
    XCTAssertEqual(ceil(root.fractionCompleted * 100), 100);

    __unused NSProgress *child5 = [NSProgress progressWithTotalUnitCount:1];
    XCTAssertEqual(ceil(root.fractionCompleted * 100), 80);
}
Mortify answered 21/4, 2014 at 17:23 Comment(3)
When I wanted to do something similar, I found I had to manually increase the totalUnitCount of the root NSProgress object. The overall progress reported okay up the hierarchy, but the total number of units at the root isn't changed unless you manually change it, as far as I can tell (NSProgress's totalUnitCount property is mutable, so I was just doing parentProgress.totalUnitCount += <whatever>; whenever I was about to becomeCurrentWithPendingUnitCount for the next task.)Manama
Hi Matt, I've been messing around with this some more. It seems that if I update the root's totalUnitCount and add another child, in that order, then the root's fractionComplete will be correct but the completedUnitCount and the messages in the root's localizedDescription and localizedAdditionalDescription are not.Mortify
* create root and 2 children * child1.completedUnitCount++ * "50% completed", "1 of 2" * child2.completedUnitCount++ * "100% completed", "2 of 2" * root.totalUnitCount = 3 * add child3 * child3.completedUnitCount++ * "66% completed", "1 of 3" 66% is right but it should be 2 of 3. If I manually set root.completedUnitCount = 2 before adding child3, then the fractionCompleted becomes incorrect.Mortify
M
5

I filed a bug with Apple and this was their response:

Engineering has determined that this issue behaves as intended based on the following information:

The reason is that we cannot update the parent's completed unit count while any member of a 'group' of children (those attached to it as child progresses while a progress is current) is unfinished. Once all members of the group are finished, we detach that group from the parent and increment the parent's completed unit count. This preserves correct accounting of completed work. The localized description method accounts for this, which is why it gives a different result than the completedUnitCount property.

If you want to track the completion of these children more closely, then you can have the parent become current more frequently with a smaller unit count. Then, when the children are completed, the parent will update its completed unit count more frequently.

Mortify answered 23/4, 2014 at 22:29 Comment(1)
Also, I should add that I still think there is a bug with NSProgress. That is that the localizedAdditionDescription is not kept up to date. I changed my code to track the children more closely based on their suggestion but there is still a point where the completedUnitCount is 2, the totalUnitCount is 3, localizedDescription is "66% completed", and localizedAdditionalDescription is "1 of 3". I replied to them asking about that. I'll update you if they reply again.Mortify
M
1

Okay, I think I know what I'm doing wrong. Before adding a new child the root must complete the in progress work and call resignCurrent. Then the root can update the totalUnitCount, becomeCurrent again with a new pending unit count and the children can do work. It seems that the completedUnitCount of the currentProgress is not updated until it resigns current. This is why the localizedAdditionalDescription is wrong. Not sure if this is by design or if it is a bug but it means that I can't rely on the completedUnitCount of the root node if I am going to dynamically add children to the progress hierarchy.

- (void)testDynamicNSProgress2
{
    NSProgress *root = [NSProgress progressWithTotalUnitCount:2];
    [root becomeCurrentWithPendingUnitCount:2];

    XCTAssertEqual(root.completedUnitCount, 0);
    NSLog(@"%@", root.localizedDescription);           // 0% completed
    NSLog(@"%@", root.localizedAdditionalDescription); // 0 of 2

    // Add two children
    NSProgress *child1 = [NSProgress progressWithTotalUnitCount:1];
    NSProgress *child2 = [NSProgress progressWithTotalUnitCount:1];

    // First child completes work    
    child1.completedUnitCount++;
    XCTAssertEqual(root.completedUnitCount, 0);
    NSLog(@"%@", root.localizedDescription);           // 50% complete
    NSLog(@"%@", root.localizedAdditionalDescription); // 1 of 2

    // Second child completes work
    child2.completedUnitCount++;
    [root resignCurrent];
    XCTAssertEqual(root.completedUnitCount, 2);
    NSLog(@"%@", root.localizedDescription);           // 100% complete
    NSLog(@"%@", root.localizedAdditionalDescription); // 2 of 2

    // Update totalUnitCount and become current again
    root.totalUnitCount = 3;
    [root becomeCurrentWithPendingUnitCount:1];
    XCTAssertEqual(root.completedUnitCount, 2);
    XCTAssertEqual(root.totalUnitCount, 3);
    NSLog(@"%@", root.localizedDescription);           // 66% completed
    NSLog(@"%@", root.localizedAdditionalDescription); // 1 of 3 (wrong! should be 2 of 3)

    // Last child completes
    NSProgress *child3 = [NSProgress progressWithTotalUnitCount:1];
    child3.completedUnitCount++;
    [root resignCurrent];
    XCTAssertEqual(root.completedUnitCount, 3);
    NSLog(@"%@", root.localizedDescription);           // 100% completed
    NSLog(@"%@", root.localizedAdditionalDescription); // 3 of 3
}
Mortify answered 21/4, 2014 at 21:11 Comment(4)
What happens if you move the resignCurrent to immediately after you've added the children? Does it make any difference?Manama
Matt, I did try that but the result is the same. :(Mortify
Okay, it was just a bit of a leap in the dark there. Not that there's much else that one can do with NSProgress at the moment :)Manama
One year later. This is finally fixed on iOS 8.Archespore

© 2022 - 2024 — McMap. All rights reserved.