В 2022 году я написал статью «Жизненный цикл UIViewController», где подробно разобрал порядок вызова методов и основные сценарии работы с ними.
С тех пор прошло больше трёх лет, и в iOS появилось несколько изменений, которые делают старую статью уже не до конца актуальной.
-
Некоторые методы вышли из практики (например, viewDidUnload или didReceiveMemoryWarning). Первый был удалён из API, второй формально остаётся частью UIViewController, но в современных версиях iOS практически не используется.
-
Добавились новые хуки (viewIsAppearing, viewSafeAreaInsetsDidChange, viewLayoutMarginsDidChange).
-
Появился полноценный Scene lifecycle для работы с многозадачностью и мультиоконностью.
-
Лучшие практики работы с Auto Layout тоже немного изменились.
В этой статье я собрал актуальное руководство по жизненному циклу UIViewController на 2025 год.
Для тех, кто хочет глубже разбираться в архитектуре iOS-приложений и следить за обновлениями экосистемы, я регулярно публикую материалы у себя в Telegram: @swiftynew.
Оглавление
-
Введение
-
Базовые этапы жизненного цикла
-
Layout cycle
-
Методы изменения окружения
-
Child View Controllers
-
State Restoration и память
-
Устаревшие методы
-
Полезные дополнения
-
Схема жизненного цикла (таблица + диаграмма)
-
Заключение
1. Введение
Жизненный цикл UIViewController — это основа iOS-разработки. Без понимания порядка вызова методов сложно правильно строить архитектуру, верстать экраны и управлять состоянием приложения.
Ещё несколько лет назад разработчики активно использовали методы вроде viewDidUnload() и didReceiveMemoryWarning(). Сегодня viewDidUnload полностью удалён из API, а didReceiveMemoryWarning хоть и остался, но в современных версиях iOS вызывается крайне редко и не играет значимой роли в управлении памятью.
Зато появились новые инструменты:
-
viewIsAppearing(_:) — более надёжный аналог viewWillAppear.
-
viewSafeAreaInsetsDidChange() и viewLayoutMarginsDidChange() — для работы с современными устройствами.
-
Поддержка SceneDelegate и multiwindow на iPad.
В этой статье я разберу все этапы жизненного цикла UIViewController, добавлю примеры и выделю что устарело, а что нужно использовать сегодня.
2.Базовые этапы жизненного цикла
Классический цикл остаётся прежним:
-
init(nibName:bundle:) / init?(coder:) — инициализация контроллера.
-
loadView() — создаётся корневая view (если без Storyboard/XIB).
-
viewDidLoad() — view загружена, здесь настраивается UI и подписки.
-
updateViewConstraints() — система обновляет констрейнты (может вызываться многократно).
-
viewWillLayoutSubviews() — вызывается перед раскладкой сабвью.
-
viewDidLayoutSubviews() — вызывается после раскладки сабвью.
-
viewWillAppear(_:) — перед каждым показом экрана.
-
viewIsAppearing(_:) (новый метод с iOS 13) — вызывается один раз в момент появления, отлично подходит для обновлений.
-
viewDidAppear(_:) — экран полностью показан.
-
viewWillDisappear(_:) — перед скрытием.
-
viewDidDisappear(_:) — экран ушёл с экрана.
-
didReceiveMemoryWarning() — вызывается при нехватке памяти; в iOS 13+ почти не используется, но формально остаётся частью API.
-
deinit — контроллер уничтожен.
3. Layout cycle
Layout cycle — это процесс, в котором система рассчитывает размеры и позиции всех сабвью внутри контроллера. Здесь важно понимать когда и где правильно обновлять констрейнты или фреймы, чтобы не допускать багов и проблем с производительностью.
Основные методы:
-
updateViewConstraints()
-
вызывается, когда системе нужно обновить констрейнты;
-
используется для пакетного обновления constraints;
-
рекомендуется вызывать setNeedsUpdateConstraints(), если состояние изменилось, и не трогать сами constraints в viewDidLayoutSubviews.
-
-
viewWillLayoutSubviews()
-
вызывается перед тем, как система начнёт раскладывать сабвью;
-
удобно обновлять данные, от которых зависит верстка (например, ориентация).
-
-
viewDidLayoutSubviews()
-
вызывается после того, как сабвью получили актуальные фреймы;
-
используется для подгонки UI, например:
-
настройка CAShapeLayer под размеры view;
-
пересчёт contentInset у UIScrollView.
-
-
final class ProfileViewController: UIViewController { private let avatarView = UIImageView(image: UIImage(named: "avatar")) override func viewDidLoad() { super.viewDidLoad() view.addSubview(avatarView) avatarView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ // Задаем констрейнты ]) } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() // Круглая аватарка после того, как view получила размеры avatarView.layer.cornerRadius = avatarView.bounds.width / 2 avatarView.layer.clipsToBounds = true } }
Best practices
-
✅Создавай и активируй constraints в viewDidLoad (или loadView), а не в viewDidLayoutSubviews.
-
✅ Для изменения constraints используй setNeedsUpdateConstraints() → updateViewConstraints().
-
✅ Для анимации constraints меняй константы и вызывай layoutIfNeeded() внутри блока UIView.animate.
-
✅ Для кастомных view обновляй frame-сабвью в layoutSubviews().
Важно: не создавай новые constraints в viewDidLayoutSubviews, иначе они будут дублироваться при каждом layout pass, что приведёт к варнингам и просадке производительности.
4. Методы изменения окружения
Современные устройства и iOS-фичи (iPad multitasking, dark mode, Dynamic Type) требуют уметь правильно реагировать на изменения окружения. Для этого у UIViewController есть специальные методы.
traitCollectionDidChange(_:)
Вызывается, когда изменяются traits контроллера:
-
size class (например, при split view на iPad),
-
темная/светлая тема,
-
динамический шрифт (Dynamic Type).
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { print("Переключение темы: светлая ↔ тёмная") } }
viewWillTransition(to:with:)
Вызывается при изменении размеров контейнера (например, поворот устройства или Split View на iPad).
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) coordinator.animate(alongsideTransition: { _ in print("Анимация изменения размера до: \(size)") }) }
viewSafeAreaInsetsDidChange()
Вызывается при изменении safe area. Это может происходить при:
-
показе/скрытии клавиатуры,
-
входящем звонке (верхний баннер),
-
смене ориентации.
override func viewSafeAreaInsetsDidChange() { super.viewSafeAreaInsetsDidChange() print("Safe area изменилась: \(view.safeAreaInsets)") }
viewLayoutMarginsDidChange()
Вызывается при изменении layoutMargins. Чаще всего — если система меняет отступы контейнера. Полезно для адаптивных интерфейсов.
override func viewLayoutMarginsDidChange() { super.viewLayoutMarginsDidChange() print("layoutMargins изменились: \(view.layoutMargins)") }
Best practices
-
✅ Для dark mode / Dynamic Type — использовать traitCollectionDidChange.
-
✅ Для поворотов и Split View — viewWillTransition(to:with:).
-
✅ Для адаптации safe area (например, пересчитать inset у ScrollView) — viewSafeAreaInsetsDidChange.
-
✅ Для тонкой подстройки верстки — viewLayoutMarginsDidChange.
Таким образом, современные методы позволяют аккуратно реагировать на любые изменения окружения, без хака в viewDidLayoutSubviews().
5. Child View Controllers
Контроллеры можно вкладывать друг в друга. Это используется при создании кастомных контейнеров (например, таббаров, пейджеров, или split-layout экранов).
Чтобы дочерние VC корректно получали события жизненного цикла, важно вызывать специальные методы.
addChild(_:)
Добавляет дочерний контроллер в контейнер.
addChild(child) // 1. сообщаем UIKit, что child добавляется view.addSubview(child.view) // 2. добавляем его view в иерархию // ... (устанавливаем констрейнты) child.didMove(toParent: self) // 3. уведомляем, что добавление завершено
⚠️ didMove(toParent:) при добавлении не вызывается автоматически — это обязанность контейнера.
removeFromParent()
Удаляет дочерний контроллер из контейнера.
child.willMove(toParent: nil) // 1. уведомляем о выходе child.view.removeFromSuperview() // 2. убираем view child.removeFromParent()
При removeFromParent() UIKit сам вызовет didMove(toParent: nil).
Управление появлением вручную
Если контейнер сам управляет children внутри себя (например, кастомный TabBarController), UIKit не всегда сам пробрасывает события viewWillAppear / viewDidAppear.
Для этого нужно использовать:
-
beginAppearanceTransition(_:animated:)
-
endAppearanceTransition()
Обычно это делается внутри методов контейнера при переключении активного child VC.
Best practices
-
✅ Добавление: addChild → addSubview (+ констрейнты) → didMove(toParent:).
-
✅ Удаление: willMove(toParent:nil) → removeFromSuperview → removeFromParent().
-
✅ Для кастомных контейнеров (пейджер/табар) управлять lifecycle детей через beginAppearanceTransition / endAppearanceTransition.
Если просто добавить child.view как сабвью без вызова этих методов → дочерний VC не будет получать lifecycle события!
6. State Restoration и память
iOS умеет сохранять и восстанавливать состояние приложения. Для этого у UIViewController есть методы state restoration. А ещё есть старый метод для работы с памятью, который сейчас практически не используется.
Сохранение состояния
-
encodeRestorableState(with:)
Сохраняет состояние контроллера (например, выбранный таб, открытая вкладка).
-
decodeRestorableState(with:)
Восстанавливает состояние из архива.
-
applicationFinishedRestoringState()
Вызывается, когда система закончила восстановление состояния для всех контроллеров.
-
didReceiveMemoryWarning()
Раньше (iOS < 13) при нехватке памяти система вызывала этот метод, и разработчики должны были вручную освобождать кэш или обнулять view. Начиная с iOS 11–13 система стала по-другому управлять памятью: при нехватке ресурсов iOS чаще выгружает представления неактивных контроллеров или завершает приложение целиком, вместо того чтобы массово рассылать memory warning. Поэтому в современных проектах метод почти не используется, и большинство разработчиков оставляют его пустым или вовсе игнорируют.
Best practices
-
✅ Используй state restoration только если приложение должно «возвращаться» в то же место (например, Safari).
-
✅ Для кэширования данных лучше использовать хранилище (UserDefaults, CoreData), а не state restoration.
-
✅ didReceiveMemoryWarning() можно игнорировать — в новых версиях iOS он не нужен.
Таким образом, в 2025 году методы encodeRestorableState(with:), decodeRestorableState(with:) и applicationFinishedRestoringState() остаются частью API и могут использоваться для state restoration (например, в приложениях с multiwindow на iPad или документ-ориентированных приложениях вроде Pages или Safari). Однако в современной практике большинство приложений не используют встроенную систему state restoration, предпочитая явное сохранение состояния через UserDefaults, CoreData, NSUserActivity или собственные механизмы.
7. Устаревшие методы
-
viewDidUnload() — Полностью удалён начиная с iOS 6. Раньше вызывался, когда view выгружалась из памяти.
-
willRotate(to:duration:), didRotate(from:) — устаревшие обработчики поворота(< iOS 8), заменены на viewWillTransition(to:with:).
-
didReceiveMemoryWarning() — не помечен как deprecated, но в iOS 13+ почти не используется. Раньше применялся для ручного освобождения кэша/ресурсов. Сегодня iOS сама выгружает view и управляет памятью. Может быть полезен только в старых приложениях (< iOS 12).
8. Полезные дополнения
loadViewIfNeeded()
-
Безопасно инициирует view, если она ещё не загружена.
-
Удобно, если нужно подготовить UI заранее, но при этом не насильно обращаться к view.
-
Если view ещё не загружена → вызывает loadView() → потом viewDidLoad()
-
Если view уже загружена → просто возвращает её (ничего не вызывает).
func attach(_ child: UIViewController) { addChild(child) child.loadViewIfNeeded() // гарантируем, что аутлеты созданы // конфигурируем публичным API/сабвью, если нужно view.addSubview(child.view) child.view.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ //настраиваем констрейнты... ]) child.didMove(toParent: self) }
systemLayoutSizeFitting(_:)
-
Метод, который рассчитывает оптимальный размер view с учётом Auto Layout.
-
Используется для self-sizing ячеек в UITableView / UICollectionView.
let targetSize = CGSize(width: view.bounds.width, height: UIView.layoutFittingCompressedSize.height) let fittingSize = customView.systemLayoutSizeFitting(targetSize) print("Высчитанная высота: \(fittingSize.height)")
sizeThatFits(_:)
-
Похож на systemLayoutSizeFitting, но работает без Auto Layout.
-
Подходит для вьюшек, которые рассчитывают размер вручную.
intrinsicContentSize
-
«Естественный» размер вью (например, UILabel возвращает размер текста).
-
Полезно для кастомных компонентов.
invalidateIntrinsicContentSize()
-
Сообщает Auto Layout, что intrinsicContentSize изменился.
-
Пример: кастомная кнопка, где изменился текст → нужно пересчитать размер.
UILayoutGuide
-
Лёгкий объект для верстки, не участвует в иерархии view.
-
Используется как «невидимая прокладка» для построения адаптивных интерфейсов.
9.Схема жизненного цикла UIViewController
|
Метод |
Когда вызывается |
Для чего использовать |
|---|---|---|
|
init(nibName:bundle:) / init?(coder:) |
При создании VC |
Инициализация, DI |
|
loadView() |
При первом доступе к view |
Создание корневой view (если без XIB/Storyboard) |
|
viewDidLoad() |
После загрузки view (1 раз) |
Настройка UI, добавление сабвью, констрейнты |
|
viewWillAppear(_:) |
Перед каждым показом |
Лёгкие обновления UI |
|
viewIsAppearing(_:) |
В момент начала анимации |
Надёжнее для подписок/обновлений |
|
updateViewConstraints() |
Когда нужны новые констрейнты |
Пакетное обновление constraints |
|
viewWillLayoutSubviews() |
Перед layout |
Подготовка к перерасчёту |
|
viewDidLayoutSubviews() |
После layout |
Подгонка UI под финальные размеры |
|
viewDidAppear(_:) |
После показа |
Старт анимаций, сетевых запросов |
|
viewWillDisappear(_:) |
Перед скрытием |
Сохранение состояния, отмена обновлений |
|
viewDidDisappear(_:) |
После скрытия |
Очистка ресурсов |
|
traitCollectionDidChange(_:) |
При смене size class / dark mode |
Адаптация UI |
|
viewWillTransition(to:with:) |
При изменении размера контейнера |
Реакция на поворот, split view |
|
viewSafeAreaInsetsDidChange() |
При изменении safe area |
Корректировка inset-ов |
|
viewLayoutMarginsDidChange() |
При изменении layoutMargins |
Тонкая адаптация UI |
|
beginAppearanceTransition / endAppearanceTransition |
При кастомных контейнерах |
Синхронизация lifecycle child |
|
didReceiveMemoryWarning() |
Актуальна для обратной совместимости iOS<12 |
Освобождает память |
|
deinit |
При уничтожении VC |
Очистка ресурсов, отписка |
Заключение
Жизненный цикл UIViewController — база на любом собеседовании.
-
Метод viewDidUnload остался в истории и был удалён из API. А didReceiveMemoryWarning хоть и присутствует до сих пор, в новых проектах фактически не используется.
-
Вместо них мы используем современные хуки: viewIsAppearing, viewSafeAreaInsetsDidChange, viewLayoutMarginsDidChange.
-
Для адаптивной верстки важны updateViewConstraints, intrinsicContentSize и UILayoutGuide.
ссылка на оригинал статьи https://habr.com/ru/articles/943778/
Добавить комментарий