Какие архитектурные решения UIKit задаёт вокруг UI state, lifecycle, navigation, threading и platform integration?
UIKit строится на UIApplication → UIWindow → UIViewController → UIView. State хранится в контроллере или модели, lifecycle управляется OS, navigation через UINavigationController/present, всё UI — на main thread.
Базовая иерархия объектов
UIKit строится на чёткой иерархии: UIApplication (singleton) → UIWindowScene → UIWindow → корневой UIViewController → дерево UIView. Понимание этой цепочки отвечает на 80% вопросов о том, «почему что-то не отображается».
UI State: где хранить
UIKit не навязывает паттерн управления состоянием, но задаёт правила: UI-state, нужный только одному экрану, хранится в контроллере или его вью-модели; state, разделяемый между экранами — в сервисном слое или глобальном хранилище (UserDefaults, CoreData, in-memory singleton).
// Локальный state — в контроллере
final class ProfileViewController: UIViewController {
// Не UIView — это бизнес-объект
private var user: User? {
didSet { updateUI() } // единственная точка обновления UI
}
private func updateUI() {
guard isViewLoaded else { return } // guard важен при async загрузке
nameLabel.text = user?.name
avatarView.configure(with: user?.avatarURL)
}
override func viewDidLoad() {
super.viewDidLoad()
Task { user = try? await userService.fetchProfile() }
}
}
Lifecycle: ключевые callback'и
override func viewDidLoad() // вью создано, bounds ещё не финальны
override func viewWillAppear(_ animated: Bool) // вью вот-вот появится
override func viewDidLayoutSubviews() // layout завершён, bounds финальны
override func viewDidAppear(_ animated: Bool) // вью видно пользователю
override func viewWillDisappear(_ animated: Bool)// момент отмены операций
override func viewDidDisappear(_ animated: Bool) // вью убрано с экрана
Правило: задавайте начальные значения в viewDidLoad; делайте frame-зависимый layout в viewDidLayoutSubviews; отменяйте Timer, NotificationCenter и задачи в viewWillDisappear или deinit.
Navigation: три модели
// 1. Stack navigation
navigationController?.pushViewController(DetailVC(), animated: true)
navigationController?.popViewController(animated: true)
// 2. Modal presentation
let vc = SettingsViewController()
vc.modalPresentationStyle = .pageSheet
present(vc, animated: true)
dismiss(animated: true)
// 3. Tab-based
let tabBar = UITabBarController()
tabBar.viewControllers = [homeNC, searchNC, profileNC]
window?.rootViewController = tabBar
Threading: всё UI — на main thread
// Неправильно — обновление UI с фонового потока
URLSession.shared.dataTask(with: url) { data, _, _ in
self.label.text = parse(data) // CRASH или undefined behavior
}.resume()
// Правильно — async/await автоматически возвращает на main actor
final class FeedViewController: UIViewController {
@MainActor
private func loadFeed() async {
do {
let items = try await feedService.fetch()
tableView.reloadData() // всегда на main actor
} catch {
showError(error)
}
}
}
Platform integration
UIKit интегрируется с платформой через системные callback'и: UIApplicationDelegate для app lifecycle (фон/передний план, push-токены, deep links), UNUserNotificationCenterDelegate для нотификаций, UISceneDelegate для multi-window на iPad. Каждый из них вызывается системой асинхронно — важно не блокировать эти методы синхронной работой.
Подводные камни
- Читать view.bounds в viewDidLoad — bounds ещё не финальны до первого layoutSubviews; frame-зависимый код нужно переносить в viewDidLayoutSubviews.
- Обновлять UI с фонового потока — UIKit не thread-safe; даже «безобидное»
label.text = ..."с фонового потока приводит к рандомным крашам. - Retain cycle в closures — захват
selfбез[weak self]в animation completion, URLSession callback или Timer удерживает контроллер после pop/dismiss. - Не отменять подписки при уходе с экрана — NotificationCenter без removeObserver (до iOS 9 и в некоторых edge cases) или Timer без invalidate приводит к вызовам на мёртвых объектах.
- Смешивать frame и AutoLayout — constraints переписывают frame на каждом layout pass; нельзя задать frame вручную и ожидать, что он сохранится при AutoLayout.
- Не понимать разницу viewWillAppear и viewDidLoad — viewDidLoad вызывается один раз, viewWillAppear — каждый раз при появлении (например, после pop с дочернего экрана); состояние, зависящее от актуальности данных, обновляется в viewWillAppear.
- Создавать вью до addSubview — AutoLayout constraint между вью из разных иерархий вызывает assertion failure; всегда добавляйте вью в иерархию до активации constraint'ов.
- Игнорировать memory warning — при
didReceiveMemoryWarningнужно освобождать кеши; игнорирование может привести к kill процесса на устройствах с малым объёмом RAM.
What hurts your answer
- Знать термины UIKit, но не понимать связи между абстракциями
- Объяснять поведение через отдельные примеры вместо причинной модели
- Не связывать mental model с диагностикой ошибок
What they're listening for
- Понимает ключевые абстракции UIKit
- Может предсказывать поведение системы через mental model
- Связывает модель с debugging и production decisions