Режим языка Swift 6 превращает компилятор в «аудитора конкурентности»: вместо поиска неправильного @MainActor или меж-Actor доступа на code review xcodebuild падает сразу. Нужен узел сборки с стабильной мощностью и зафиксированной версией — полная компиляция без соседского джиттера CPU на хвосте задержек. Материал показывает поэтапное подключение строгих проверок Swift 6 на выделенных удалённых узлах Mac NUKCLOUD и фиксацию в воротах слияния PR.
00Зачем мигрировать сейчас — и зачем выделенные узлы
Строгие проверки конкурентности Swift 6 (-strict-concurrency=complete) масштабируются с размером кодовой базы: при первом включении у среднего iOS-проекта 200–800 ошибок компиляции — обычное дело. Исправления тянутся неделями и месяцами; в этот период CI должен гарантировать:
- Зафиксированные минорные версии Xcode: формулировки диагностики изоляции Actor меняются между версиями Xcode; фиксация делает вывод CI сопоставимым.
- Без конкуренции за CPU с соседями: полная компиляция со strict-concurrency на 20–40% дольше обычной debug-сборки; выделенные узлы стабилизируют P95 хвоста задержек.
- Изолированные пространства имён DerivedData: несколько PR с проверками конкурентности нуждаются в отдельных инкрементальных кэшах, иначе результаты ненадёжны.
01Три уровня проверки конкурентности и путь миграции
В Swift три уровня флагов компилятора в OTHER_SWIFT_FLAGS (Xcode Build Settings) или swiftSettings в Package.swift:
| Флаг | Смысл | Рекомендуемый этап |
|---|---|---|
-strict-concurrency=minimal | Проверяет только базовое соответствие синтаксиса async/await, без границ Actor | Разогрев legacy-кода (недели 1–2) |
-strict-concurrency=targeted | Проверяет границы Actor для типов с явными Sendable, @MainActor и т. п. | Пофайловое исправление (недели 3–6) |
-strict-concurrency=complete | Полная проверка, эквивалент режима языка Swift 6; необработанный совместный доступ в конкурентности — ошибка | В ворота после достижения 0 ошибок |
На практике мигрируйте по модулям: сначала complete на листовых модулях, затем на ядре. В SwiftPM .enableUpcomingFeature("StrictConcurrency") точно задаёт уровень для каждого target.
02Границы изоляции Actor: три частых типа ошибок
Около 90% ошибок компиляции в период миграции — из этих трёх категорий; их исправление снимает большую часть «красных» сборок:
① Меж-Actor доступ к non-Sendable типам
// ❌ Ошибка: доступ к @MainActor извне контекста @MainActor
actor DataFetcher {
func load() async {
let result = await networkClient.fetch()
// ❌ passing argument of non-sendable type 'UserModel' across actor boundary
await MainActor.run { viewModel.update(result) }
}
}
Исправление: сделать UserModel соответствующим Sendable (value-типы часто подходят автоматически) или выполнять преобразование на границе Actor внутри границы.
② Пропущенный @MainActor
Подклассы UIKit/SwiftUI ViewController и ObservableObject до миграции неявно работали в главном потоке; Swift 6 требует явной аннотации @MainActor. Кнопка Fix в Xcode массово правит, но каждое место лучше проверить вручную, чтобы не перенести на главный поток логику, которой там быть не должно.
③ Конкурентный доступ к глобальным переменным
// ❌ В Swift 6 глобалы должны быть Sendable или nonisolated
var sharedCache: NSCache<NSString, AnyObject> = .init()
// ✅ nonisolated(unsafe) на переходный период
nonisolated(unsafe) var sharedCache: NSCache<NSString, AnyObject> = .init()
// ✅ Лучше: обернуть в Actor с async API
actor CacheStore {
private let cache = NSCache<NSString, AnyObject>()
func object(forKey key: String) -> AnyObject? { cache.object(forKey: key as NSString) }
}
03Настройка ворот CI на удалённом Mac
Суть ворот для PR: отдельный Job для проверок конкурентности, не смешивать с обычными тестами. Тогда сбой проверки не засоряет отчёт тестов, а на этапе исправлений можно задать warn-only или block-merge. На NUKCLOUD: тарифы, затем заказ — не полагайтесь на временные всплески пула по минутам в миграции.
Окно исправлений часто длится неделями: логируйте счётчик ошибок конкурентности, wall time полной сборки и размер DerivedData, связывайте с изменением spec.
name: Swift 6 Concurrency Gate
on:
pull_request:
branches: [main, release/*]
jobs:
concurrency-check:
runs-on: self-hosted # Выделенный удалённый Mac Runner NUKCLOUD
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Выбрать Xcode (зафиксировать minor)
run: sudo xcode-select -s /Applications/Xcode_16.3.app
- name: Restore DerivedData cache
uses: actions/cache@v4
with:
path: ~/DerivedData/swift6-check
key: swift6-${{ runner.name }}-${{ hashFiles('**/*.xcodeproj/project.pbxproj', '**/Package.resolved') }}
restore-keys: swift6-${{ runner.name }}-
- name: Build with strict concurrency (complete)
run: |
xcodebuild build \
-project MyApp.xcodeproj \
-scheme MyApp \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath ~/DerivedData/swift6-check \
OTHER_SWIFT_FLAGS="-strict-concurrency=complete" \
| xcbeautify --renderer github-actions
- name: Upload build log on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: swift6-build-log-${{ github.sha }}
path: ~/DerivedData/swift6-check/Logs/Build/
-derivedDataPath ~/DerivedData/swift6-check отдельно от обычных сборок, чтобы артефакты проверки конкурентности не портили debug-кэш. При нескольких PR добавьте runner.name или номер PR для дополнительной изоляции.04Шестишаговый Runbook постепенной миграции
-
01
Снимок базовой линии: на выделенном удалённом Mac выполните полную сборку с
-strict-concurrency=minimal, зафиксируйте число ошибок и время компиляции как измеримую отправную точку. -
02
Сначала листовые модули: в графе SwiftPM выберите target без зависимых downstream и по одному включайте
complete. После исправления каждого target — отдельный PR с обозримым diff. -
03
Стратегия веток CI: на долгоживущей ветке
feature/swift6-migrationзапускайте Job проверки конкурентности какwarn-only(continue-on-error: true), не блокируя ежедневные слияния PR. Переводите модуль на block-merge только при 0 ошибок. -
04
Зафиксируйте версию Xcode: запишите версию
xcode-selectв Runbook и README, чтобы локальные Mac разработчиков и узлы CI использовали один минор — меньше шума «локально прошло, в CI упало». -
05
Дашборд тренда ошибок: пишите число ошибок Swift 6 за каждый прогон CI в структурированные логи или Build Metrics, отслеживайте миграцию линейными графиками и недельными целями «снизить на X ошибок».
-
06
Приёмка полных ворот: когда все модули на 0 ошибок, включите Require swift6-concurrency-check to pass в Branch Protection для
mainи задайтеSWIFT_VERSION = 6во всех target — формальный переход на режим языка Swift 6.
05Локальный Mac и выделенный удалённый узел
Миграции Swift 6 нужны и быстрые локальные итерации, и достоверная полная проверка в CI; требования к узлам различаются:
| Измерение | Локальный Mac разработчика | Выделенный удалённый Mac NUKCLOUD (CI) |
|---|---|---|
| Версия Xcode | На усмотрении разработчика, возможен разброс | Минор зафиксирован скриптом, едино для команды |
| DerivedData | Общий с ежедневной разработкой; инкрементальный кэш может испортиться | Отдельные пути, «ведра» по PR или Runner |
| Доступность CPU | Конкуренция с другими приложениями, время сборки плавает | Bare-metal выделенный, предсказуемый P95 задержек |
| Доверие к результату | «Локально прошло» не означает прохождение CI | Окончательный арбитр на этапе ворот |
| Лучше всего для | Быстрые пробы одного модуля и чтение диагностики Xcode | Полные ворота проверки конкурентности и ежедневная регрессия |
06Часто задаваемые вопросы
-strict-concurrency=complete — флаг компилятора, его можно включить в режиме языка Swift 5, чтобы увидеть все ошибки до переключения. Формальный режим Swift 6 (SWIFT_VERSION = 6) ещё убирает устаревший синтаксис. Сначала исправьте по флагу complete, затем переключайте режим языка..unsafeFlags(["-strict-concurrency=minimal"]) в Package.swift или SWIFT_STRICT_CONCURRENCY = minimal в hook post_install Podfile. Большинство крупных библиотек завершили миграцию к 2026 году — сначала смотрите Release Notes.--concurrent-jobs N и разнесите DerivedData по PR (-derivedDataPath ~/DerivedData/pr-${{ github.event.number }}), чтобы Job компилировались независимо. Выделенные bare-metal узлы сильны здесь: нет соседской борьбы за CPU и низкая дисперсия между параллельными Job.-strict-concurrency=complete на недели мультирегиональные bare-metal Mac / cloud Mac узлы NUKCLOUD стабильнее — с runbook консоли для приёмки.