Рефакторинг legacy кода на Swift: пошаговое руководство для iOS/macOS

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: почему legacy код на Swift требует рефакторинга

Работа с унаследованным (legacy) кодом — неизбежная часть жизни iOS/macOS разработчика. Даже если проект изначально писался на Swift, со временем он обрастает техническим долгом: устаревшие паттерны, отсутствие тестов, монолитная архитектура и смешение Swift с Objective-C. Рефакторинг такого кода не просто улучшает читаемость — он снижает стоимость поддержки, ускоряет добавление новых фич и уменьшает количество багов.

В этой статье мы разберём конкретные техники рефакторинга legacy Swift-кода: от избавления от force unwrapping до миграции с MVC на MVVM. Все примеры можно применить как для iOS, так и для macOS проектов.

1. Оценка и подготовка: с чего начать рефакторинг

Прежде чем менять код, важно понять его текущее состояние. Используйте встроенные инструменты Xcode и метрики.

1.1. Анализ предупреждений и ошибок

Откройте проект в Xcode и перейдите в Report Navigator (Cmd+9). Обратите внимание на:

  • Yellow warnings — устаревшие API (deprecated), неиспользуемые переменные.
  • Red errors — синтаксические ошибки, которые могут быть следствием неполной миграции Swift.
  • SwiftLint/SwiftFormat — подключите линтер, чтобы автоматически выявить нарушения code style.

1.2. Поиск "красных флагов" в коде

Пробегитесь по проекту с помощью поиска (Cmd+Shift+F) по следующим паттернам:

  • ! — force unwrapping (опциональные значения, которые крашат приложение).
  • as! — force casting (приведение типов без проверки).
  • try! — игнорирование ошибок.
  • NSObject — наследование от NSObject без необходимости (чистый Swift этого не требует).
  • #selector — строковые селекторы, которые лучше заменить на #selector с типом.
// Плохо (legacy)
let name = user.name!  // force unwrap — краш при nil

// Хорошо (рефакторинг)
guard let name = user.name else {
    return
}

1.3. Написание тестов перед изменениями

Рефакторинг без тестов — это риск. Если тестов нет, начните с их добавления для критических модулей. Используйте XCTest для unit-тестов и XCUITest для UI.

Пример простого теста для legacy функции:

class LegacyCalculatorTests: XCTestCase {
    func testCalculateTotal() {
        let calculator = LegacyCalculator()
        let result = calculator.calculateTotal(items: [10, 20, 30])
        XCTAssertEqual(result, 60)
    }
}

2. Основные техники рефакторинга Swift-кода

Перейдём к практическим шагам, которые можно применить прямо сейчас.

2.1. Замена force unwrapping на безопасные паттерны

Самая частая проблема в legacy коде — повсеместное использование !. Заменяйте их на guard let, if let или ?? (nil-coalescing).

// До рефакторинга
func getUserName() -> String {
    return user.name!
}

// После рефакторинга
func getUserName() -> String {
    return user.name ?? "Unknown"
}

2.2. Избавление от Massive View Controller (MVC → MVVM)

Legacy проекты часто страдают от "толстых" контроллеров. Выделите бизнес-логику в отдельные сервисы или ViewModel.

// Legacy ViewController (фрагмент)
class ProfileViewController: UIViewController {
    @IBOutlet weak var nameLabel: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Бизнес-логика прямо в контроллере
        let user = DataManager.shared.fetchUser()
        nameLabel.text = user.name?.capitalized
    }
}

// После рефакторинга
class ProfileViewModel {
    private let userService: UserService
    
    var userName: String {
        return userService.currentUser?.name?.capitalized ?? "Guest"
    }
    
    init(userService: UserService) {
        self.userService = userService
    }
}

class ProfileViewController: UIViewController {
    @IBOutlet weak var nameLabel: UILabel!
    private let viewModel = ProfileViewModel(userService: UserService())
    
    override func viewDidLoad() {
        super.viewDidLoad()
        nameLabel.text = viewModel.userName
    }
}

2.3. Миграция с Objective-C совместимости на чистый Swift

Если в проекте остались классы, наследующие NSObject только ради совместимости с ObjC, перепишите их на чистые Swift-структуры или классы без @objc.

// Legacy (ObjC-совместимость)
class LegacyModel: NSObject {
    @objc var id: Int = 0
    @objc var title: String = ""
}

// Рефакторинг
struct RefactoredModel {
    let id: Int
    let title: String
}

3. Инструменты Xcode для автоматизации рефакторинга

Xcode предоставляет мощные встроенные инструменты, которые ускоряют рефакторинг.

3.1. Refactor → Rename

Используйте Editor → Refactor → Rename (или Cmd+Ctrl+E) для переименования символов с учётом всех зависимостей. Это безопаснее ручного поиска и замены.

3.2. Extract Method

Выделите повторяющийся код в отдельные функции. Кликните правой кнопкой на блок кода → Refactor → Extract to Method.

3.3. Convert to Swift Concurrency

Если legacy код использует completion handlers или делегаты, Xcode 15+ может автоматически конвертировать их в async/await. Выберите метод → Refactor → Convert to Async.

// До: completion handler
func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
    // ...
}

// После: async/await
func fetchData() async throws -> Data {
    // ...
}

4. Пошаговый план рефакторинга legacy модуля

Предположим, у вас есть старый класс UserManager с 500 строками, множеством force unwrap и прямой работой с UserDefaults. Вот как его можно реоргани

Блоги

Рекомендаций по книгам