Как использовать / тестировать изменения пользовательской информации NSProgress дочернего экземпляра NSProgress
Я реализую NSProgress
Поддержка в библиотеке, и я написал несколько модульных тестов, чтобы проверить, что все работает правильно. Хотя в идеале я хотел бы иметь возможность передавать некоторые дополнительные метаданные (userInfo
ключи не используются NSProgress
сам, но для пользователей моего API, чтобы потреблять), а пока я просто пытаюсь получить localizedDescription
а также localizedAdditionalDescription
работать так, как говорит документация, они должны. Поскольку метод, который я тестирую, извлекает файлы из архива, я установил kind
в NSProgressKindFile
и установить различные ключи, связанные с файловыми операциями (например, NSProgressFileCompletedCountKey
).
Я ожидаю, когда я наблюдаю изменения в localizedDescription
с KVO, что я буду видеть такие обновления:
Обработка "Тестового файла A.txt"
Обработка "Тестового файла B.jpg"
Обработка "Тестового файла C.m4a"
Когда я останавливаюсь на точке останова и po
localizedDescription
на работника NSProgress
пример (childProgress
ниже), это фактически то, что я вижу. Но когда мои тесты запускаются, все, что они видят, это следующее, подразумевая, что он не видит ничего из userInfo
ключи, которые я установил:
0% выполнено
0% выполнено
53% завершено
Выполнено на 100%
Выполнено на 100%
Похоже, userInfo
ключи я установил на ребенка NSProgress
экземпляр не передается его родителю, даже если fractionCompleted
делает. Я делаю что-то неправильно?
Ниже приведены некоторые фрагменты абстрактного кода, но вы также можете загрузить коммит с этими изменениями из GitHub. Если вы хотите воспроизвести это поведение, запустите -[ProgressReportingTests testProgressReporting_ExtractFiles_Description]
а также -[ProgressReportingTests testProgressReporting_ExtractFiles_AdditionalDescription]
контрольные примеры.
В моем тестовом классе:
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];
}
}
Затем в моем классе в тесте:
- (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
{
}
2 ответа
Хорошо, у меня была возможность еще раз взглянуть на код с большим количеством кофе в моей системе и большим количеством времени в моих руках. Я на самом деле вижу, как это работает.
В вашем методе testProgressReporting_ExtractFiles_AdditionalDescription я изменил код так:
NSProgress *extractFilesProgress = [NSProgress progressWithTotalUnitCount:1];
[extractFilesProgress setUserInfoObject:@10 forKey:NSProgressEstimatedTimeRemainingKey];
[extractFilesProgress setUserInfoObject:@"Test" forKey:@"TestKey"];
И тогда в наблюдаем ValueForKeyPath я напечатал эти объекты:
po progress.userInfo {
NSProgressEstimatedTimeRemainingKey = 10;
TestKey = Test;
}
po progress.localizedAdditionalDescription
0 of 1 — About 10 seconds remaining
Вы можете увидеть значения ключей, которые я добавил, и localizedAdditionalDescription был создан на основе этих записей (обратите внимание на оставшееся время). Итак, все выглядит так, как будто работает правильно.
Я думаю, что одна путаница может быть связана со свойствами NSProgress и их влиянием на значения ключей в dict userInfo. Установка свойств не добавляет значения ключа к пользователю userInfo, а установка значений ключа не устанавливает свойства. Например, установка вида хода выполнения не добавляет NSProgressFileOperationKindKey к диктовке userInfo. Значение в userInfo dict, если оно присутствует, является в большей степени переопределением свойства, которое используется только при создании localizedAdditionalDescription.
Вы также можете увидеть пользовательское значение ключа, которое я добавил. Итак, все это выглядит так, будто все работает правильно. Можете ли вы указать мне на что-то, что все еще отвлекает?
Я хотел прокомментировать ответ @clarus, но ТАК не позволяет мне делать удобочитаемое форматирование в комментарии. TL;DR - их понимание всегда было моим пониманием, и это то, что меня поразило, когда я начал работать с NSProgress
несколько лет назад.
Для подобных вещей мне нравится проверять код Swift Foundation на предмет подсказок реализации. Возможно, это не на 100% авторитетно, если что-то еще не сделано, но мне нравится видеть общее мышление.
Если вы посмотрите на реализацию setUserInfoObject(: forKey:)
Вы можете видеть, что реализация просто устанавливает пользовательскую информацию dict, не передавая ничего до родителя.
И наоборот, обновления, которые влияют на дочернюю дробь, завершаются явным образом обратным вызовом (private) _parent
свойство, указывающее его состояние, должно обновляться в ответ на дочернее изменение.
Это личное _updateChild(: from: to: portion:)
только кажется, что обновление завершенной дроби, а не что-либо, связанное с пользовательским информационным словарем