如何 use/test NSProgress 子 NSProgress 实例的 userInfo 更改
How to use/test NSProgress userInfo changes of a child NSProgress instance
我正在库中实现 NSProgress
支持,并且我编写了一些单元测试来测试一切是否正常工作。虽然理想情况下我希望能够传递一些额外的元数据(userInfo
键未被 NSProgress
本身使用,但供我的 API 的用户使用),但现在我是只是想让 localizedDescription
和 localizedAdditionalDescription
像文档中说的那样工作。由于我正在测试的方法是从存档中提取文件,因此我将 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:)
似乎只关心更新已完成的部分,而不是与用户信息字典相关的任何内容。
我正在库中实现 NSProgress
支持,并且我编写了一些单元测试来测试一切是否正常工作。虽然理想情况下我希望能够传递一些额外的元数据(userInfo
键未被 NSProgress
本身使用,但供我的 API 的用户使用),但现在我是只是想让 localizedDescription
和 localizedAdditionalDescription
像文档中说的那样工作。由于我正在测试的方法是从存档中提取文件,因此我将 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:)
似乎只关心更新已完成的部分,而不是与用户信息字典相关的任何内容。