如何 use/test NSProgress 子 NSProgress 实例的 userInfo 更改

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

我正在库中实现 NSProgress 支持,并且我编写了一些单元测试来测试一切是否正常工作。虽然理想情况下我希望能够传递一些额外的元数据(userInfo 键未被 NSProgress 本身使用,但供我的 API 的用户使用),但现在我是只是想让 localizedDescriptionlocalizedAdditionalDescription 像文档中说的那样工作。由于我正在测试的方法是从存档中提取文件,因此我将 kind 设置为 NSProgressKindFile 并设置了与文件操作相关的各种键(例如 NSProgressFileCompletedCountKey)。

我希望当我观察到 KVO 对 localizedDescription 的更改时,我会看到这样的更新:

Processing “Test File A.txt”

Processing “Test File B.jpg”

Processing “Test File C.m4a”

当我在断点处停止并且 po worker NSProgress 实例上的 localizedDescription(下面的 childProgress)时,这实际上就是我所看到的。但是当我测试 运行 时,他们只看到以下内容,这意味着它没有看到我设置的任何 userInfo 键:

0% completed

0% completed

53% completed

100% completed

100% completed

我在子 NSProgress 实例上设置的 userInfo 键似乎没有传递给它的父实例,尽管 fractionCompleted 传递了。我做错了什么吗?

我在下面给出了一些抽象代码片段,但您也可以下载包含这些更改的提交 from GitHub。如果您想重现此行为,运行 -[ProgressReportingTests testProgressReporting_ExtractFiles_Description]-[ProgressReportingTests testProgressReporting_ExtractFiles_AdditionalDescription] 测试用例。

在我的测试用例中 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];
    }
}

然后,在我的class测试下:

- (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;
    }
}

在我递增 childProgress.completedUnitCount 时,这就是 userInfo 在调试器中的样子。我设置的字段都表示:

> po childProgress.userInfo

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

当每个 KVO 通知返回时,notificationProgress.userInfo 看起来是这样的:

> po notificationProgress.userInfo

{
}

好的,我有机会再次查看代码,我的系统里有更多的咖啡和更多的时间。我实际上看到它在工作。

在您的 testProgressReporting_ExtractFiles_AdditionalDescription 方法中,我将代码更改为:

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

然后在 observeValueForKeyPath 中,我打印了这些对象:

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

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

您可以看到我添加的键值,localizedAdditionalDescription 是根据这些条目创建的(注意剩余时间)。所以,这一切看起来都在正常工作。

我认为围绕 NSProgress 属性及其对 userInfo 字典中的键值的影响可能会造成混淆。设置属性不会将键值添加到 userInfo dict,设置键值不会设置属性。例如,设置进度种类不会将 NSProgressFileOperationKindKey 添加到 userInfo 字典。 userInfo 字典中的值(如果存在)更像是 属性 的重写,仅在创建 localizedAdditionalDescription 时使用。

您还可以看到我添加的自定义键值。所以,这一切看起来都在正常工作。你能告诉我一些仍然看起来不对劲的地方吗?

我想对@clarus 的回答发表评论,但不允许我在评论中进行可读格式设置。 TL;DR - 他们的理解一直是我的理解,当我几年前开始与 NSProgress 合作时,这让我很痛苦。

对于此类内容,我喜欢查看 Swift Foundation 代码以获取实现提示。如果事情还没有完成,它可能不是 100% 权威,但我喜欢看到一般的想法。

如果您查看 setUserInfoObject(: forKey:) 的实现,您会发现该实现只是简单地设置了用户信息字典,而没有向父级传播任何内容。

相反,影响子项分数的更新已完成 explicitly call back 到(私有)_parent 属性 以指示其状态应更新以响应子项更改。

那个私有 _updateChild(: from: to: portion:) 似乎只关心更新已完成的部分,而不是与用户信息字典相关的任何内容。