Контекст: почему это больно
Swift 6 вышел в сентябре 2024 года и принёс одно большое изменение которое коснулось всех — строгая проверка конкурентности теперь включена по умолчанию. Если раньше вы могли передавать @MainActor-изолированные значения в фоновый поток и компилятор только предупреждал, теперь это ошибка компиляции.
В нашем проекте при первом переключении на Swift 6 mode мы получили 847 ошибок. Это звучит страшно, но на деле большинство из них — одна и та же проблема в разных местах.
Таксономия ошибок
Почти все ошибки которые мы встретили делятся на три категории:
- Sendable violations — передача non-Sendable типов через границы акторов
- MainActor isolation — обращение к UI из фонового контекста
- Callback capture — захват изолированного состояния в completion handlers
Разбираемся с Sendable
Самая частая ошибка выглядит вот так:
Решений несколько. Если класс действительно должен быть доступен из разных потоков — делаем его @MainActor:
Если же логика в нём не привязана к UI, переходим на актор:
MainActor и completion handlers
Второй по частоте тип проблем — старый код с completion handlers который обращается к UI без явного переключения на главный поток:
Правильное решение — мигрировать на async/await и явно аннотировать контекст:
Стратегия миграции
Мы использовали следующий подход:
- Включили Swift 6 mode только для одного модуля — самого маленького листового
- Исправили все ошибки в нём, не трогая остальные
- Поднялись по дереву зависимостей вверх
- На каждом шаге делали PR и ждали зелёного CI
Итог
Миграция заняла около трёх спринтов. 847 ошибок → 0. Основной вывод: большинство проблем — это либо добавить @MainActor, либо сделать тип Sendable, либо переписать callback на async. Ничего принципиально сложного, просто много механической работы.
Зато теперь у нас нет целого класса багов связанных с гонками данных, и компилятор страхует нас от их появления в будущем.