В 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.
rusl002
Поясните пожалуйста, как didReceiveMemoryWarning официально устарел, если в документации сказано, что настоятельно рекомендуется его реализовать
Dinozavr2005 Автор
Спасибо за замечание,
didReceiveMemoryWarning() действительно до сих пор присутствует в API и формально не помечен как deprecated. Однако начиная с iOS 11–13 система стала по-другому управлять памятью: при нехватке ресурсов iOS чаще выгружает представления неактивных контроллеров или завершает приложение целиком, вместо того чтобы массово рассылать memory warning. Поэтому в современных проектах метод почти не используется, и большинство разработчиков оставляют его пустым или вовсе игнорируют.
Формулировка «Your app should implement this method to release any additional memory…» действительно есть в документации. Она появилась ещё во времена iOS 6–8, когда приложения часто падали от нехватки памяти, и каждый контроллер обязан был чистить тяжёлые ресурсы (картинки, кэши, массивы данных). Apple не убрала эту рекомендацию из текста, потому что она актуальна для обратной совместимости и для приложений, поддерживающих старые версии iOS или работающих с действительно массивными кэшами.
Вы правы, я некорректно выразился в статье, и формулировки лучше поправить, чтобы не вводить читателей в заблуждение. Корректнее будет сказать, что метод не устарел официально.
spiceginger
Даже не совсем так. WWDC сессия по управлению памятью рекомендует НЕ освобождать данные в массивах и тд потому что система использует сжатие памяти и если начать трогать объекты в массивах(и тд) даже для того что бы удалить это может привести к увеличению потребления памяти так как системе придется разжать эту область памяти. Поэтому рекомендуется на время например перестать что то кэшировать и тд и обнулить ссылки на объекты которые больше не нужны и могут быть легко восстановлены (Это относится и к структурам данных, обнулить их можно. Итерироваться что б найти например не нужные данные не стоит).