Команда записала количество задач в основном приложении, но виджет на удалённом симуляторе по-прежнему показывает старое значение. Дополнительные вызовы обновления в такой ситуации обычно лишь усложняют диагностику: запись общих данных, их чтение расширением, построение таймлайна и вывод на экран системой — четыре отдельных этапа. На примере виджета со счётчиком задач разберём, как проверять каждый этап независимо.
Определите границу передачи данных
Основное приложение и расширение виджета работают в разных процессах. Обычный UserDefaults.standard не становится для них общим источником данных автоматически. Сначала включите одну и ту же App Group для обоих target в проекте и поместите идентификатор группы в конфигурацию, доступную обеим сторонам. Не закрепляйте тестовый идентификатор в конфигурации релизной сборки.
После записи рабочих данных основное приложение сохраняет количество и время обновления в UserDefaults(suiteName:). Расширение читает их из набора с тем же именем. Функция записи должна возвращать результат или ошибку, а не молча оставлять старое значение. Если расширению не удаётся прочитать данные, оно должно показывать явно обозначенное состояние по умолчанию. Если данные состоят не из одного целого числа, кодируйте их как единую запись с номером версии. Это исключит сочетание уже обновлённого количества со старым заголовком.
Запрос на обновление лишь сообщает системе, что данные могли измениться. Он не переносит данные из основного приложения в расширение и не гарантирует немедленную перерисовку виджета.
После записи сначала прочитайте значение обратно в основном приложении, затем зафиксируйте при чтении в расширении номер версии без конфиденциальных данных. Если версии различаются, проблема в передаче данных. Если версии совпадают, а изображение — нет, переходите к проверке таймлайна и отображения. Не записывайте в диагностические журналы содержимое задач, данные учётной записи или общую запись целиком.
Сделайте расчёт таймлайна чистой функцией
Содержимое элементов таймлайна не должно зависеть от реального времени выполнения теста. Пусть провайдер только читает данные и вызывает функцию расчёта, передавая ей текущее время и прочитанный снимок данных. Код ниже показывает минимальное разделение обязанностей; типы можно разместить в исходном файле, общем для основного приложения и расширения.
import Foundation
struct CountEntry: Equatable {
let date: Date
let count: Int
let revision: Int
}
func makeEntries(
now: Date,
count: Int,
revision: Int
) -> [CountEntry] {
[
CountEntry(date: now, count: count, revision: revision),
CountEntry(
date: now.addingTimeInterval(15 * 60),
count: count,
revision: revision
)
]
}
Второй элемент по-прежнему содержит тот же снимок данных: из того, что прошло пятнадцать минут, нельзя сделать вывод об изменении количества задач. Получив элементы, провайдер может задать для таймлайна стратегию повторного запроса после последнего элемента. Она указывает желаемое время проверки, а не обещает обновление интерфейса с точностью до секунды. Когда появляются новые рабочие данные, запросите перезагрузку виджета соответствующего вида. Если продукту нужен точный обратный отсчёт, пересмотрите требование, а не имитируйте процесс реального времени частыми элементами таймлайна.
Фиксированные входные данные и проверяемый результат
В модульном тесте задайте фиксированные Date, количество и номер ревизии. Проверьте, что создано ровно два элемента, их даты строго возрастают, а количество и ревизия в обоих элементах совпадают. Отдельно проверьте граничный случай с нулевым количеством: ноль должен отображаться как настоящее значение, а не перекрываться значением по умолчанию для ошибки чтения. Такие тесты не запускают симулятор и не ждут системного планировщика, поэтому сбой сразу указывает на логику расчёта.
Проверьте реальную передачу данных в облачном симуляторе
Найдите доступный симулятор iOS и его идентификатор устройства командой xcrun simctl list devices available. При сборке и тестировании фиксируйте целевой ID, чтобы после смены исполнителя поиск по имени устройства не выбрал другую среду выполнения:
xcodebuild test \
-project Widgets.xcodeproj \
-scheme Widgets \
-destination "platform=iOS Simulator,id=$SIM_ID" \
-resultBundlePath "$PWD/widget-tests.xcresult"
Имя проекта и scheme здесь приведены для примера: перед запуском замените их на реально существующие в репозитории. Значение SIM_ID должно явно передаваться через конфигурацию задачи. Для каждого запуска используйте новый каталог результатов, иначе существующий .xcresult может привести к преждевременному завершению команды с ошибкой. После успешного прохождения модульных тестов таймлайна установите сборку с основным приложением и расширением и добавьте виджет в симуляторе. Сначала проверьте состояние по умолчанию, затем запишите новую ревизию из основного приложения и запросите обновление. В конце сопоставьте запись, прочитанную расширением, с изображением на экране.
При проверке интерфейса оценивайте только итоговое содержимое и номер ревизии. Не делайте условие «появилось через несколько секунд после запроса» жёстким утверждением теста. Система может менять время обновления. Если нужно точно проверить немедленное отображение, тестируйте управляемое представление предварительного просмотра внутри основного приложения, а не принимайте системное планирование за таймер приложения.
Ищите причину по симптомам, а не повторяйте переустановку
| Наблюдение | Что проверить в первую очередь |
|---|---|
| Уже в основном приложении значение после чтения неверно | Момент записи, имя набора и обработку ошибок |
| В основном приложении значение верное, расширение всё ещё читает старое | Конфигурацию App Group у обоих target и версию записи |
| Расширение читает новое значение, но элементы остаются старыми | Аргументы, передаваемые провайдеру, и построение таймлайна |
| Новые элементы верны, но изображение остаётся старым | Вид виджета, значения, используемые представлением, и фактическое время вывода системой |
До начала диагностики запишите коммит проекта, версию среды выполнения, ID устройства и ревизию общей записи. Это полезнее скриншота, на котором «ничего не изменилось». Не стирайте весь симулятор сразу после каждой неудачи: вместе с ним исчезнут и общие данные, позволяющие воспроизвести проблему. Если сброс необходим, сначала сохраните результаты тестов и нужные журнальные записи без конфиденциальных данных, затем осознанно выберите удаление данных приложения или пересоздание симулятора.
Передайте критерии приёмки в конвейер
Разделите автоматизацию на два уровня. Первый запускает тесты чистой функции и предотвращает регрессии в порядке элементов, обработке нуля и номере ревизии. Второй на фиксированном симуляторе проверяет, что приложение и расширение установлены и читают одну и ту же запись; макет проверяется вручную либо через управляемый предварительный просмотр. Фактическое обновление виджета учитывайте отдельно как наблюдение, не смешивая его с детерминированными проверками первого уровня.
В конце проверьте три вещи: оба ли target указывают на одну общую группу, есть ли у новых данных отслеживаемый номер ревизии, не раскрывающий содержимое, и передаётся ли время в тесты как входной параметр вместо ожидания реальных минут. Если всё это выполнено, при следующем появлении старого изображения можно последовательно проверить данные, элементы и отображение, не пытаясь угадать причину повторными запросами на обновление.
Часто задаваемые вопросы
Почему после запроса обновления виджет всё ещё показывает старое значение?
Запрос не гарантирует немедленного выполнения. Сначала проверьте значение в общем контейнере и результат чтения провайдером; не считайте фиксированное время ожидания критерием ошибки.
Нужно ли ждать обновления симулятора при тестировании таймлайна?
Нет. Передайте время и данные в чистую функцию и проверьте порядок и содержимое записей. Симулятор используйте для проверки установки, передачи данных и итогового отображения.
Подключите облачный Mac с фиксированной конфигурацией к своему рабочему процессу
Арендуйте выделенные физические узлы на день, неделю, месяц или квартал для сборок, автоматизации и задач инференса MLX на фиксированных аппаратных ресурсах.