UIKitSeniorExperience

Как понять, что проблема в приложении связана с UIKit, а не с backend, сетью, дизайном состояния или платформой?

Изолируйте слой: создайте минимальный VC с заглушкой данных — если баг исчез, проблема в данных/сети. Затем проверьте thread через dispatchPrecondition, используйте Instruments Core Animation для FPS и Memory Graph Debugger для retain cycles.

Диагностика: как локализовать источник проблемы

Когда в iOS-приложении что-то ломается, первый шаг — построить гипотезу о слое, где возникает проблема, и затем опровергнуть или подтвердить её минимальным экспериментом. Наугад переписывать UIKit-код без понимания причины — антипаттерн.

Слои системы и инструменты диагностики

  • Backend/API: проверьте ответы через Charles Proxy или Proxyman — перехватите реальные HTTP-запросы, сравните payload с ожиданиями. Отключите кэш URLSession (URLRequest.cachePolicy = .reloadIgnoringLocalCacheData) и убедитесь, что данные корректны до рендеринга.
  • Сеть: используйте Network Link Conditioner (Developer settings на устройстве) для симуляции плохой связи. Проверьте URLSessionTaskMetrics на время установки соединения и TTFB.
  • UIKit rendering: включите Debug → Color Blended Layers и Color Offscreen-Rendered в Simulator для поиска лишних blending слоёв. Используйте Xcode Instruments → Core Animation для замера FPS и найдите drop ниже 60 fps.
  • State management: добавьте breakpoint на viewWillAppear / viewDidLoad и логируйте состояние ViewModel. Если состояние корректно, но UI отображает старые данные — проблема в binding между моделью и view.
  • Платформа/OS: воспроизведите на разных iOS версиях через Simulator или BrowserStack. Apple изменяла поведение UIScrollView contentInsetAdjustmentBehavior в iOS 11, UINavigationBar appearance в iOS 15 — платформенные изменения ломают код, который «работал раньше».

Практический алгоритм

  1. Воспроизведите проблему стабильно. Нестабильно воспроизводимая проблема — вероятно, race condition или retain cycle.
  2. Изолируйте: создайте минимальный ViewController с заглушкой данных (без сети, без CoreData). Если баг исчез — проблема в данных или сети, не в UIKit.
  3. Проверьте thread: добавьте dispatchPrecondition(condition: .onQueue(.main)) перед любым обновлением UI. UIKit не thread-safe — обновление с background queue даёт визуальные артефакты без crash.
  4. Используйте Instruments → Allocations и Leaks для поиска утечек памяти, которые могут вызывать потерю состояния.
  5. Сравните поведение на симуляторе и реальном устройстве — Metal рендеринг, точность тачей и производительность отличаются.
import UIKit
import os.log

final class DiagnosticViewController: UIViewController {
    private let logger = Logger(subsystem: "com.app", category: "DiagnosticVC")

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        // Проверяем, что мы на main thread
        dispatchPrecondition(condition: .onQueue(.main))
        logger.debug("viewWillAppear: state=\(self.debugDescription)")
    }

    func updateUI(with data: SomeModel) {
        // Явная диспетчеризация на main queue
        DispatchQueue.main.async { [weak self] in
            guard let self else { return }
            // weak self предотвращает retain cycle в closure
            self.render(data)
        }
    }

    private func render(_ data: SomeModel) {
        // Все UIKit-обновления здесь, на main thread
        titleLabel.text = data.title
    }
}

Платформенные нюансы, которые ломают не по вине UIKit

Начиная с iOS 15, UINavigationBar использует UINavigationBarAppearance по умолчанию. Код, который задавал navigationBar.barTintColor напрямую, перестал работать — это не баг UIKit, а изменение API. Аналогично, UITableView в iOS 15 получил sectionHeaderTopPadding = 22 по умолчанию — это сломало множество list-экранов, которые не задавали явный стиль.

Подводные камни

  • Обновление UILabel/UIImageView с фонового потока не вызывает crash сразу — баг проявляется позже в виде артефактов или некорректного layout.
  • Charles Proxy показывает HTTP-трафик, но не URLSession из WKWebView — там нужен отдельный перехват.
  • Instruments → Leaks не находит retain cycles в Swift closures — для этого нужен Memory Graph Debugger (Debug → Memory Graph в Xcode).
  • UIKit-проблема на симуляторе может не воспроизводиться на устройстве из-за разного поведения Metal и OpenGL ES.
  • Изменение contentInset у UIScrollView вместо adjustedContentInset — источник «плавающего» контента при появлении клавиатуры.
  • Сброс состояния ViewController при memory warning — viewDidUnload больше не вызывается (deprecated iOS 6), но didReceiveMemoryWarning всё ещё работает и может обнулить кэши.
  • NSZombie включённый в схеме замедляет приложение в 3–5 раз — не забудьте выключить перед performance-профилированием.
  • Разные результаты Auto Layout на iPhone SE (320pt) и iPhone Pro Max (430pt) — добавляйте constraint priority и тестируйте на обоих размерах.

What hurts your answer

  • Сразу обвинять UIKit, не проверив соседние слои системы
  • Чинить симптом без минимального воспроизведения и evidence
  • Не учитывать версии, конфигурацию, окружение и recent changes

What they're listening for

  • Умеет локализовать проблему вокруг UIKit
  • Двигается от симптома к гипотезам и проверкам
  • Отличает баг инструмента от ошибки использования или окружения

Related topics