Миграция iOS-приложения с Objective-C на Swift
Миграция — не переписывание. «Давайте всё перепишем на Swift» с нуля — это рискованный путь, который ломает накопленную бизнес-логику и теряет нюансы поведения, которые не задокументированы. Правильная миграция — постепенная, файл за файлом, с сохранением поведения.
Почему миграцию откладывают и зачем всё же делать
Откладывают из-за страха: ObjC/Swift bridging boundary — это место, где тихо ломаются nullable/nonnull аннотации, NS_SWIFT_NAME переименования путают, а generic-типы не пробрасываются через заголовок. Страх понятен.
Делают потому, что новые Apple API выходят с Swift Concurrency (async/await), SwiftUI, Observation framework — и без миграции к ним либо не подберёшься, либо получаешь уродливые обёртки. Плюс: Swift-компилятор ловит категорию ошибок (force unwrap на nil, data race через Sendable) до рантайма.
Как мы подходим к миграции
Шаг 1: аудит. Составляем граф зависимостей между классами. Ищем листовые узлы — классы, которые ни от чего не зависят (утилиты, модели данных, сервисы). С них начинаем.
Шаг 2: аннотации в ObjC-заголовках. Перед миграцией любого класса расставляем NS_ASSUME_NONNULL_BEGIN/END в .h файлах, отмечаем nullable там, где это реально nullable. Это сразу показывает, где в Swift будут Optional, а где нет. Пропуск этого шага приводит к String? везде, где должен быть String.
Шаг 3: миграция моделей. NSObject-подклассы с properties превращаются в Swift struct (если value semantics подходит) или class (если нужна идентичность или наследование). @objc атрибут нужен только там, где модель всё ещё используется из ObjC-кода — не везде.
Шаг 4: сервисы и network layer. Completion-handler-based API переписываем на async/await через withCheckedContinuation или withCheckedThrowingContinuation. Старый ObjC-калбек:
func fetchUser(id: String, completion: @escaping (User?, Error?) -> Void)
Превращается в:
func fetchUser(id: String) async throws -> User
ObjC-код, вызывающий этот метод, продолжает работать через __attribute__((swift_async(...))) или через промежуточный ObjC-враппер.
Шаг 5: ViewController'ы. Самые сложные. Здесь IBOutlet, IBAction, delegate паттерны, notification observers. Мигрируем последними, когда большинство зависимостей уже на Swift. Переносим логику во ViewModel (чистый Swift), ViewController оставляем тонким.
Главные ловушки
@objc inflate. После миграции ViewModel разработчик добавляет @objc dynamic к property для поддержки KVO из старого ObjC-кода. Swift-компилятор перестаёт проверять типы для этих свойств как Swift. Решение: уходим от KVO к Combine или @Observable (iOS 17+) и убираем @objc dynamic.
Bridging header bloat. Большой ProjectName-Bridging-Header.h с десятками #import замедляет компиляцию. По мере миграции удаляем ненужные импорты — компиляция заметно ускоряется.
Тесты. ObjC unit-тесты (XCTest) работают в Swift-таргете без изменений. Но если тест тестирует внутренние методы ObjC-класса через @testable import, при миграции этого класса могут измениться уровни доступа. Готовимся адаптировать тесты параллельно с миграцией.
Кейс из практики: мобильное banking-приложение, ~80 000 строк ObjC, команда из 3 iOS-разработчиков. Мигрировали за 4 месяца по приоритету: сначала сетевой слой и модели (вышли на async/await и убрали callback pyramid), затем сервисы (авторизация, аналитика, хранение), последними — экраны. В результате краши по ObjC-related исключениям снизились на 70% — просто потому, что компилятор стал ловить force-unwrap ошибки раньше рантайма.
Что входит в работу
- Аудит кодовой базы и составление плана миграции по приоритетам
- Расстановка nullable/nonnull аннотаций в ObjC-заголовках
- Поэтапная миграция: модели → сервисы → ViewModels → UI
- Перевод completion handlers на
async/await - Адаптация unit-тестов
- Code review и проверка ObjC/Swift boundary на каждом этапе
Сроки
| Объём кодовой базы | Ориентировочные сроки |
|---|---|
| До 10 000 строк ObjC | 2–3 недели |
| 10 000–50 000 строк | 1–2 месяца |
| 50 000+ строк | 2–3 месяца и более |
Сроки зависят от количества ObjC/Swift boundary точек, наличия тестов и готовности команды участвовать в ревью. Стоимость рассчитывается индивидуально после аудита кодовой базы.







