Проверка прогресса iOS: веса этапов, отмена и завершение
При пакетной обработке файлов индикатор доходит до ста процентов, а затем появляется ошибка. Пользователь нажимает отмену, но фоновая задача всё равно публикует результат. Обычно причина не в отрисовке, а в том, что объём работы, сигнал отмены и конечное состояние операции смешаны воедино. На облачном Mac GNUMac эти контракты можно сначала закрепить модульными тестами без сети и симулятора, а затем проверить интерфейс iOS.
Сначала определите, что означает прогресс
В качестве тестового сценария возьмём преобразование четырёх входных файлов с последующей проверкой двух выходных. На преобразование приходится шестьдесят процентов общего объёма работы, на проверку — сорок. Эти веса заданы тестом: они не получены измерением производительности и не отражают оставшееся время.
Общий объём родительского прогресса зададим равным ста, а объём дочерних — в количестве файлов. Нельзя напрямую складывать число преобразованных файлов и количество загруженных байтов: разные единицы сначала нужно соотнести с весами в родительском прогрессе.
Состояние операции описывается отдельно: ожидание, выполнение, отмена в процессе, отменено, ошибка и успех. Объект прогресса отвечает только за количественные показатели, а не за классификацию ошибок.
Процент отвечает на вопрос «Какая доля оговорённого объёма работы выполнена?», а состояние операции — «Доставлен ли результат надёжным образом?». Одно не заменяет другое.
До реализации зафиксируйте три ограничения: каждый дочерний прогресс присоединяется только один раз; выполненный объём не превышает общий; перед фиксацией результата обязательно проверяется отмена. Родительский и дочерние объекты принадлежат одной задаче, а не хранятся в глобальных переменных, общих для всех задач.
Проверяйте веса детерминированными тестами
Создайте тестовый файл в существующем таргете модульных тестов проекта и добавьте следующий код. Тест использует только Foundation и XCTest: ему не нужно ждать реального преобразования файлов или подбирать порядок выполнения с помощью задержек.
import Foundation
import XCTest
final class WeightedProgressTests: XCTestCase {
func testWeightedStages() {
let root = Progress(totalUnitCount: 100)
let convert = Progress(totalUnitCount: 4)
let verify = Progress(totalUnitCount: 2)
root.addChild(convert, withPendingUnitCount: 60)
root.addChild(verify, withPendingUnitCount: 40)
XCTAssertEqual(root.fractionCompleted, 0, accuracy: 0.000001)
convert.completedUnitCount = 2
XCTAssertEqual(root.fractionCompleted, 0.3, accuracy: 0.000001)
verify.completedUnitCount = 1
XCTAssertEqual(root.fractionCompleted, 0.5, accuracy: 0.000001)
convert.completedUnitCount = 4
verify.completedUnitCount = 2
XCTAssertEqual(root.fractionCompleted, 1, accuracy: 0.000001)
}
}
Этапы здесь намеренно продвигаются вперемежку, чтобы проверить: родительский прогресс учитывает не только текущий этап. Числа с плавающей точкой сравниваются с допуском, а не через проверки процентов, отформатированных в строки.
Запустите этот класс из навигатора тестов на облачном Mac и сохраните хеш коммита, результаты тестов и сведения об использованном наборе инструментов. Если тесты уже запускаются из командной строки, добавьте в существующий запуск только этот тестовый таргет — отдельная матрица симуляторов для проверки прогресса не нужна.
Отмена требует участия исполнителя
Progress.cancel() передаёт сигнал, но не прерывает работающий цикл принудительно. Проверяйте состояние перед обработкой каждой единицы работы, а счётчик увеличивайте только после её фактического завершения. Перед окончательной заменой выходного файла нужна ещё одна проверка.
Следующий тест проверяет, что после отмены следующая единица работы не запускается. Он не претендует на покрытие гонок при конкурентном выполнении; его можно добавить прямо в предыдущий тестовый класс.
func testCancellationStopsNextUnit() {
let progress = Progress(totalUnitCount: 4)
progress.isCancellable = true
var processed = 0
func runNextUnit() {
guard !progress.isCancelled else { return }
processed += 1
progress.completedUnitCount += 1
}
runNextUnit()
progress.cancel()
runNextUnit()
XCTAssertTrue(progress.isCancelled)
XCTAssertEqual(processed, 1)
XCTAssertEqual(progress.completedUnitCount, 1)
}
Для асинхронного исполнителя нужны ещё две управляемые точки приостановки: перед началом длительной операции и перед фиксацией результата. Тест приостанавливает задачу, отправляет отмену, затем разрешает продолжить выполнение и проверяет, что новая работа не началась, а окончательный результат не был опубликован. Координируйте выполнение с помощью управляемых тестом барьеров, а не фиксированных задержек.
Если нижележащая операция предоставляет интерфейс отмены, передавайте ей сигнал одновременно с отменой задачи. Если прервать её нельзя, дождитесь возврата, отбросьте результат и удалите временные файлы. Интерфейс должен оставаться в состоянии «Отмена…», не обещая пользователю, что работа уже остановлена.
Не маскируйте неизвестный объём работы и повторные попытки
Пока сканирование не определило общее число файлов, показывайте «Подсчёт файлов…», а не пытайтесь вычислить процент при нулевом общем объёме. После сканирования зафиксируйте список задач, выстройте полную иерархию родительского и дочерних объектов и начинайте обработку.
Постоянное увеличение знаменателя во время выполнения приводит к откату прогресса назад. Если задачи действительно нужно обнаруживать динамически, показывайте состояние этапа или число обработанных единиц и явно указывайте, что текущий процент относится только к этапу с уже известным объёмом.
При повторных попытках также не следует снова и снова обнулять один и тот же дочерний прогресс. На уровне бизнес-логики можно хранить номер попытки, а на уровне прогресса — накапливать успешно завершённые логические единицы. Неудачная попытка не означает, что обработан ещё один файл.
Отдельно определите поведение при отсутствии входных данных. Если пустой ввод допустим, сразу возвращайте успешный результат «Обработка не требуется». Если вызывающая сторона обязана передать входные данные, сообщайте об ошибке до запуска, не создавая задачу, которая навсегда останется в начальной точке.
Доведите контракт до интерфейса и постоянных проверок
При получении уведомлений об изменении прогресса обновляйте состояние интерфейса на главном акторе. Каждому запуску назначайте отдельный идентификатор задачи и сверяйте его при записи обновлений, чтобы запоздавшие уведомления предыдущего запуска не перезаписали новый прогресс.
Процент для отображения можно округлять, но округлённое значение нельзя использовать как признак успеха. Если после обработки последнего файла ещё требуются проверка и атомарная фиксация результата, заранее выделите этим шагам долю работы. При ошибке сохраняйте состояние ошибки, а не доводите счётчик до максимума принудительно.
Перед слиянием изменений проверьте каждый пункт:
- Проверки промежуточных взвешенных значений и чередования этапов проходят независимо от скорости выполнения.
- Для отмены до начала работы, во время обработки и перед фиксацией результата есть отдельные тесты.
- Ошибочные пути не превращаются в успешные, а временные выходные файлы удаляются.
- После запуска новой задачи уведомления от старой не могут изменить текущий интерфейс.
- Для пустого ввода, неизвестного общего объёма и повторных попыток определены явные правила отображения.
Итог — не просто визуально плавный индикатор, а проверяемый контракт состояний. Сначала сделайте счётчики достоверными, затем отмену — управляемой, и наконец добейтесь того, чтобы успех означал только одно: результат действительно готов к использованию.
Часто задаваемые вопросы
Означают ли 100 % успешное завершение операции?
Нет. Счётчик отражает объём выполненной работы. Проверку и передачу итогового результата нужно учитывать в отдельном состоянии операции.
Останавливает ли cancel() выполняемую работу автоматически?
Нет. Исполнитель должен проверять отмену, передавать её базовой операции и повторно проверять перед фиксацией результата.
Настройте выделенный Mac mini под свой рабочий процесс
Выберите модель, узел и срок аренды под задачи проекта. Сначала проверьте требования к памяти, хранилищу и подключению, затем оформите заказ. Фактическая доступность отображается в консоли в реальном времени.