На что вы обращаете внимание при код-ревью iOS-приложений?

Ответ

При код-ревью iOS-приложений фокус идёт на качество, безопасность и поддерживаемость кода.

Ключевые аспекты:

  1. Архитектура и дизайн

    • Соблюдение принципов SOLID.
    • Чистое разделение слоёв (MVVM, VIPER, Clean Architecture).
    • Отсутствие Massive View Controller.
    • Правильное использование dependency injection.
  2. Безопасность

    • Чувствительные данные (токены, пароли) хранятся в Keychain, а не в UserDefaults или plist-файлах.
    • Использование HTTPS с правильной конфигурацией SSL/TLS (ATS).
    • Валидация и санитизация пользовательского ввода.
  3. Производительность и память

    • Отсутствие retain cycles (использование weak/unowned для захвата в замыканиях).
    • Выбор оптимальных структур данных (Array vs Set vs Dictionary).
    • Выполнение тяжелых операций (сеть, обработка изображений) в background-очередях.
    • Корректное использование инструментов (Instruments) для поиска утечек.
  4. UI/UX и доступность

    • Корректная работа Auto Layout constraints.
    • Поддержка Dynamic Type для адаптации текста под настройки пользователя.
    • Настройка accessibility-атрибутов (labels, hints, traits).
  5. Тестируемость

    • Код разделён на мелкие, тестируемые модули.
    • Использование протоколов для создания моков в unit-тестах.
    • Наличие покрытия критической бизнес-логики тестами.
  6. Стиль кода и читаемость

    • Следование соглашениям по именованию (Swift API Design Guidelines).
    • Соблюдение принципа DRY (Don't Repeat Yourself).
    • Размер методов и функций (желательно не более 40-50 строк).
    • Читаемость и понятность кода как для текущей команды, так и для будущих разработчиков.

Пример проблемного кода и улучшение:

// ❌ Проблемы: retain cycle, сохранение в UserDefaults, блокирование main queue
func loadData() {
    API.shared.getData { [weak self] result in
        DispatchQueue.main.async {
            self?.data = result
            self?.tableView.reloadData()
            self?.saveToUserDefaults(result) // Небезопасно для чувствительных данных
        }
    }
}
// ✅ Улучшенная версия
func loadData() {
    // 1. Выносим сетевой вызов в отдельный сервис
    dataService.fetchData { [weak self] result in
        switch result {
        case .success(let data):
            // 2. Обработка данных в background queue
            self?.processDataOnBackgroundQueue(data)
        case .failure(let error):
            // 3. Централизованная обработка ошибок
            self?.handleError(error)
        }
    }
}

private func processDataOnBackgroundQueue(_ data: DataModel) {
    DispatchQueue.global(qos: .userInitiated).async { [weak self] in
        let processedData = self?.dataProcessor.transform(data)
        // 4. Сохранение в безопасное хранилище (например, Core Data или Keychain для чувствительных данных)
        self?.secureStorage.save(processedData)
        // 5. Обновление UI только на main queue
        DispatchQueue.main.async {
            self?.updateUI(with: processedData)
        }
    }
}

Ответ 18+ 🔞

Давай я тебе на пальцах, как для полного чайника, объясню, на что смотреть, когда тебе подсовывают на ревью код какого-нибудь iOS-приложения. Чтобы потом не было мучительно больно за потраченные нервы и забитый багами релиз.

На что орать в первую очередь:

  1. Архитектура и как всё устроено Тут главный вопрос: не превратился ли твой ViewController в такого монстра, что он и сеть дергает, и в базу пишет, и UI рисует, и ещё кофе тебе варит? Это называется Massive View Controller, и это пиздец как плохо. Код должен быть разделён по слоям, как хороший торт, а не как перемешанная солянка. Зависимости должны передаваться извне, а не создаваться внутри — это называется dependency injection, и это не просто модное словечко, а чтобы потом не охуеть, когда всё посыпется.

  2. Безопасность, ёпта! Если я вижу, что какой-то гений засунул токен авторизации или пароль в UserDefaults или, боже упаси, в открытый plist-файл — это сразу красная карточка. Всё, что хоть чуть-чуть пахнет секретностью, — только в Keychain, и точка. И да, сетевые запросы только по HTTPS, а то потом будешь удивляться, почему у пользователей аккаунты уплыли.

  3. Производительность и память Самая частая лажа — это retain cycles, когда два объекта держат друг друга за жопу и никогда не отпустят. Память течёт, приложение падает. Смотри, чтобы в замыканиях (closures) использовались [weak self] или [unowned self], где это уместно. Тяжёлые операции — работа с сетью, обработка картинок — должны делаться в фоне, а не на главном потоке, иначе интерфейс будет висеть, как будто его ебал медведь.

  4. Внешний вид и доступность Автолейаут не должен выдавать warnings, как сумасшедший. И проверь, адаптирован ли текст под настройки системы (Dynamic Type), а то какой-нибудь дед с плохим зрением не сможет прочитать твои гениальные сообщения. Не забудь про accessibility — подпиши элементы для VoiceOver, чтобы не было просто «кнопка», а было «кнопка “Отправить сообщение”».

  5. А можно это протестировать? Если весь код свален в одну кучу и сплетён в один комок, как старые наушники в кармане, то написать unit-тесты будет невозможно. Код должен состоять из независимых кусочков, которые можно подменить заглушками (моками). Если критическая логика не покрыта тестами — это повод задуматься.

  6. Читаемость и стиль Имена переменных и функций должны быть понятными. fetchUserProfileAndThenUpdateUIAndSaveToCache — это уже перебор, а func a() — это вообще ни о чём. Следуй гайдлайнам. И, ради всего святого, не копипасть один и тот же код в десяти местах. Вынеси в отдельную функцию или утилитный класс. DRY, блядь, Don't Repeat Yourself!

Смотри, как бывает:

// ❌ Вот так делать НЕ НАДО. Типичный код-говно.
// Проблемы: утечка памяти (retain cycle), сохранение чего попало в UserDefaults, и блокировка интерфейса.
func loadData() {
    API.shared.getData { [weak self] result in
        DispatchQueue.main.async {
            self?.data = result
            self?.tableView.reloadData()
            self?.saveToUserDefaults(result) // Ты в своём уме? Сюда токены пихать?!
        }
    }
}
// ✅ А вот так — уже похоже на правду. Разложено по полочкам.
func loadData() {
    // 1. Сетевой слой — отдельно. Не дергаем синглтон напрямую из контроллера.
    dataService.fetchData { [weak self] result in
        switch result {
        case .success(let data):
            // 2. Обработка данных — в фоне, не грузим главный поток.
            self?.processDataOnBackgroundQueue(data)
        case .failure(let error):
            // 3. Ошибки обрабатываем централизованно, а не размазываем по коду.
            self?.handleError(error)
        }
    }
}

private func processDataOnBackgroundQueue(_ data: DataModel) {
    DispatchQueue.global(qos: .userInitiated).async { [weak self] in
        let processedData = self?.dataProcessor.transform(data)
        // 4. Сохраняем не абы куда, а в нормальное, безопасное хранилище.
        self?.secureStorage.save(processedData)
        // 5. И ТОЛЬКО ПОСЛЕ ВСЕГО обновляем UI, и обязательно на главной очереди!
        DispatchQueue.main.async {
            self?.updateUI(with: processedData)
        }
    }
}

Вот и вся магия. Смотришь на эти пункты, и сразу видно, где коллега сэкономил два часа, но создал тебе и всем, кто будет после, проблем на две недели. Не стесняйся указывать на косяки — от этого всем только лучше.