UIKitMiddleExperience

Какие архитектурные решения 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) → UIWindowSceneUIWindow → корневой 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

Related topics