Как понять, что проблема в приложении связана с 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 изменяла поведение
UIScrollViewcontentInsetAdjustmentBehavior в iOS 11,UINavigationBarappearance в iOS 15 — платформенные изменения ломают код, который «работал раньше».
Практический алгоритм
- Воспроизведите проблему стабильно. Нестабильно воспроизводимая проблема — вероятно, race condition или retain cycle.
- Изолируйте: создайте минимальный ViewController с заглушкой данных (без сети, без CoreData). Если баг исчез — проблема в данных или сети, не в UIKit.
- Проверьте thread: добавьте
dispatchPrecondition(condition: .onQueue(.main))перед любым обновлением UI. UIKit не thread-safe — обновление с background queue даёт визуальные артефакты без crash. - Используйте Instruments → Allocations и Leaks для поиска утечек памяти, которые могут вызывать потерю состояния.
- Сравните поведение на симуляторе и реальном устройстве — 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
- Двигается от симптома к гипотезам и проверкам
- Отличает баг инструмента от ошибки использования или окружения