Міграція 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 точок, наявності тестів та готовності команди учать в ревью. Вартість розраховується індивідуально після аудиту кодової бази.







