При переводе iOS-проекта, который поддерживается уже много лет, на Swift 6 опаснее всего не медленный темп миграции, а одновременное включение нового языкового режима, после которого сотни диагностических сообщений о конкурентности смешиваются с изменениями бизнес-логики. Надёжнее сначала создать отдельную проверку в облачном Mac: зафиксировать набор инструментов, изолировать каталог сборки, записать текущее число предупреждений, а затем постепенно ужесточать правила для каждого модуля.
Сначала разделите миграцию на два этапа
На первом этапе по-прежнему используется языковой режим Swift 5, но параметр SWIFT_STRICT_CONCURRENCY устанавливается в complete. Такая проверка выявляет передачу типов, не соответствующих Sendable, через границы изоляции, обращения к коду, изолированному главным актором, и небезопасное глобальное изменяемое состояние, не включая при этом сразу все семантические изменения Swift 6.
Только на втором этапе очищенные модули переводятся на Swift 6. Начинать рекомендуется с базовых пакетов с минимальным числом зависимостей, а затем переходить к сетевому слою, слою данных и интерфейсу. После стабилизации конечных модулей число диагностических сообщений в вышележащих модулях обычно также сокращается.
| Этап | Языковой режим | Настройка проверки | Условие слияния |
|---|---|---|---|
| Поиск проблем | Swift 5 | complete | Число предупреждений конкурентности не увеличивается |
| Очистка модуля | Swift 5 | complete | В целевом модуле нет предупреждений |
| Окончательный переход | Swift 6 | Строгие правила по умолчанию | Все сборки и тесты проходят успешно |
Цель контроля миграции конкурентности — не устранить все предупреждения в первый день, а с первого дня прекратить добавление новых проблем в главную ветку.
Создайте воспроизводимую задачу проверки
Задача на облачном Mac должна явно записывать версии Xcode и компилятора Swift, а также идентификатор коммита. Не следует повторно использовать DerivedData, оставленные после ручных сборок разработчиков: старый кеш модулей может скрыть реальные зависимости. Для каждого коммита нужен отдельный каталог, который после завершения задачи очищается согласно политике хранения.
#!/bin/zsh
set -o pipefail
export LC_ALL=C
root="$PWD/.ci"
derived="$root/DerivedData"
result="$root/ConcurrencyCheck.xcresult"
log="$root/concurrency.log"
rm -rf "$derived" "$result"
mkdir -p "$root"
xcodebuild -version
xcrun swiftc --version
git rev-parse HEAD
xcodebuild \
-project Example.xcodeproj \
-scheme Example \
-configuration Debug \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath "$derived" \
-resultBundlePath "$result" \
SWIFT_VERSION=5.0 \
SWIFT_STRICT_CONCURRENCY=complete \
build 2>&1 | tee "$log"
build_status=${pipestatus[1]}
exit "$build_status"
Значения project, scheme и целевая платформа должны браться из конфигурации репозитория, а не дублироваться в нескольких сценариях. Если проект использует workspace, замените -project на -workspace и убедитесь, что общая схема добавлена в систему контроля версий.
Подключите бюджет предупреждений к существующей главной ветке
В старых проектах обычно невозможно сразу устранить все предупреждения. После первого запуска сохраните число диагностических сообщений о конкурентности и назначьте его первоначальным бюджетом. Все последующие коммиты, превышающие бюджет, должны завершать проверку с ошибкой. После исправления каждой группы проблем уменьшайте бюджет в том же запросе на слияние.
budget=${CONCURRENCY_WARNING_BUDGET:-0}
count=$(grep -Ec \
'warning:.*(Sendable|actor-isolated|concurrency-safe)' \
.ci/concurrency.log || true)
printf 'concurrency warnings: %s, budget: %s
' "$count" "$budget"
test "$count" -le "$budget"
Сопоставление текста подходит для временного контроля, однако требует фиксированного значения LC_ALL=C. Кроме того, при каждом обновлении Xcode необходимо проверять формулировки диагностических сообщений. Пакет результатов сборки следует сохранять как артефакт неудачной проверки, чтобы не полагаться только на обрезанный журнал терминала. Когда бюджет снизится до нуля, можно напрямую запрещать любые предупреждения о конкурентности.
Не допускайте загрязнения результатов сгенерированным кодом
Если клиенты API или модели создаются генератором, его версию необходимо зафиксировать в конфигурации репозитория. Внешний сгенерированный код, который нельзя изменить немедленно, можно вынести в отдельный целевой модуль, но не следует исключать из строгой проверки весь бизнес-модуль. Иначе новый код, написанный вручную, также сможет обходить контроль.
Исправляйте проблемы по категориям, а не отключайте их массово
При появлении диагностики Sendable сначала определите, действительно ли данные должны передаваться между задачами. Для конфигурации только для чтения предпочтительны неизменяемые снимки в виде значимых типов. Ссылочные типы с изменяемым кешем можно поместить в actor. Для объектов синхронизации, которые должны использоваться совместно, следует явно инкапсулировать блокировку и определить границы доступа.
Состояние интерфейса нужно изолировать с помощью @MainActor на уровне конкретного типа или метода. Не следует помечать весь слой данных или все протоколы как @MainActor только ради успешного прохождения проверки. Это неявно перенесёт фоновую работу обратно в главный поток и создаст ещё больше обращений через границы изоляции.
@unchecked Sendable подходит только для типов, безопасность которых уже обеспечивается внутренними блокировками, неизменяемой архитектурой или последовательной очередью. При проверке кода необходимо ответить как минимум на три вопроса: какие поля защищены, проходят ли все операции чтения и записи через одну и ту же границу и как будут защищаться поля, добавленные в будущем. Если ответить на эти вопросы нельзя, использовать такое объявление не следует.
При преобразовании обратных вызовов в async также необходимо убедиться, что continuation возобновляется ровно один раз. Тесты должны охватывать как отсутствие обратного вызова, так и его повторное выполнение. Недостаточно лишь добиться исчезновения сообщения компилятора.
Переводите модули по отдельности и сохраняйте подтверждения приёмки
Перед переводом каждого модуля сначала устраните в нём все предупреждения в режиме Swift 5 complete и только затем включайте Swift 6. Приёмка должна включать как минимум сборки Debug и Release, модульные тесты и ключевые интеграционные тесты. Одной Debug-сборки для симулятора недостаточно, чтобы обнаружить проблемы в оптимизированных конфигурациях или ветвях условной компиляции.
Для каждой миграции рекомендуется записывать название модуля, ответственного, коммит перехода, оставшиеся исключения и путь к пакету результатов. При обновлении Xcode сначала создайте новую базовую линию в отдельной ветке. Не объединяйте обновление набора инструментов, исправления конкурентности и новые бизнес-функции в одном изменении.
Сохраняйте задачу строгой проверки и после завершения миграции. Swift 6 усиливает ограничения по умолчанию, однако обновления зависимостей, границы Objective-C и сгенерированный код всё ещё могут повторно создавать пробелы в изоляции. Стабильный итог — не однократное достижение нулевого числа предупреждений, а подтверждение при каждом слиянии, что регрессий нет.
Часто задаваемые вопросы
Нужно ли сразу переводить весь проект в режим Swift 6?
Нет. Сначала включите complete-проверку конкурентности в режиме Swift 5, исправьте нижние модули, а затем переводите цели по одной.
Можно ли массово применять @unchecked Sendable?
Нет. Атрибут допустим только при доказанной безопасности через неизменяемость, блокировку или последовательную изоляцию и требует отдельного ревью.
Как внедрить проверку в старый проект без остановки разработки?
Зафиксируйте текущее число предупреждений как лимит, запрещайте его рост и уменьшайте лимит после каждой исправленной группы проблем.
Выберите срок аренды облачного Mac в соответствии с длительностью задачи
Две конфигурации M4 доступны для аренды посуточно, понедельно, помесячно или поквартально; актуальные сведения об узлах и доступности отображаются в консоли в реальном времени.