В 2022 году я написал статью «Жизненный цикл UIViewController», где подробно разобрал порядок вызова методов и основные сценарии работы с ними.

С тех пор прошло больше трёх лет, и в iOS появилось несколько изменений, которые делают старую статью уже не до конца актуальной.

  • Некоторые методы вышли из практики (например, viewDidUnload или didReceiveMemoryWarning). Первый был удалён из API, второй формально остаётся частью UIViewController, но в современных версиях iOS практически не используется.

  • Добавились новые хуки (viewIsAppearing, viewSafeAreaInsetsDidChange, viewLayoutMarginsDidChange).

  • Появился полноценный Scene lifecycle для работы с многозадачностью и мультиоконностью.

  • Лучшие практики работы с Auto Layout тоже немного изменились.

В этой статье я собрал актуальное руководство по жизненному циклу UIViewController на 2025 год.

Для тех, кто хочет глубже разбираться в архитектуре iOS-приложений и следить за обновлениями экосистемы, я регулярно публикую материалы у себя в Telegram: @swiftynew.

Оглавление

  1. Введение

  2. Базовые этапы жизненного цикла

  3. Layout cycle

  4. Методы изменения окружения

  5. Child View Controllers

  6. State Restoration и память

  7. Устаревшие методы

  8. Полезные дополнения

  9. Схема жизненного цикла (таблица + диаграмма)

  10. Заключение

1. Введение

Жизненный цикл UIViewController — это основа iOS-разработки. Без понимания порядка вызова методов сложно правильно строить архитектуру, верстать экраны и управлять состоянием приложения.

Ещё несколько лет назад разработчики активно использовали методы вроде viewDidUnload() и didReceiveMemoryWarning(). Сегодня viewDidUnload полностью удалён из API, а didReceiveMemoryWarning хоть и остался, но в современных версиях iOS вызывается крайне редко и не играет значимой роли в управлении памятью.

Зато появились новые инструменты:

  • viewIsAppearing(_:) — более надёжный аналог viewWillAppear.

  • viewSafeAreaInsetsDidChange() и viewLayoutMarginsDidChange() — для работы с современными устройствами.

  • Поддержка SceneDelegate и multiwindow на iPad.

В этой статье я разберу все этапы жизненного цикла UIViewController, добавлю примеры и выделю что устарело, а что нужно использовать сегодня.

 2.Базовые этапы жизненного цикла

Классический цикл остаётся прежним:

  1. init(nibName:bundle:) / init?(coder:) — инициализация контроллера.

  2. loadView() — создаётся корневая view (если без Storyboard/XIB).

  3. viewDidLoad() — view загружена, здесь настраивается UI и подписки.

  4. updateViewConstraints() — система обновляет констрейнты (может вызываться многократно).

  5. viewWillLayoutSubviews() — вызывается перед раскладкой сабвью.

  6. viewDidLayoutSubviews() — вызывается после раскладки сабвью.

  7. viewWillAppear(_:) — перед каждым показом экрана.

  8. viewIsAppearing(_:) (новый метод с iOS 13) — вызывается один раз в момент появления, отлично подходит для обновлений.

  9. viewDidAppear(_:) — экран полностью показан.

  10. viewWillDisappear(_:) — перед скрытием.

  11. viewDidDisappear(_:) — экран ушёл с экрана.

  12. didReceiveMemoryWarning() — вызывается при нехватке памяти; в iOS 13+ почти не используется, но формально остаётся частью API.

  13. deinit — контроллер уничтожен.

3. Layout cycle

Layout cycle — это процесс, в котором система рассчитывает размеры и позиции всех сабвью внутри контроллера. Здесь важно понимать когда и где правильно обновлять констрейнты или фреймы, чтобы не допускать багов и проблем с производительностью.

Основные методы:

  1. updateViewConstraints()

    • вызывается, когда системе нужно обновить констрейнты;

    • используется для пакетного обновления constraints;

    • рекомендуется вызывать setNeedsUpdateConstraints(), если состояние изменилось, и не трогать сами constraints в viewDidLayoutSubviews.

  2. viewWillLayoutSubviews()

    • вызывается перед тем, как система начнёт раскладывать сабвью;

    • удобно обновлять данные, от которых зависит верстка (например, ориентация).

  3. 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
Жизненный цикл UIViewController

Заключение


Жизненный цикл UIViewController — база на любом собеседовании.

  • Метод viewDidUnload остался в истории и был удалён из API. А didReceiveMemoryWarning хоть и присутствует до сих пор, в новых проектах фактически не используется.

  • Вместо них мы используем современные хуки: viewIsAppearing, viewSafeAreaInsetsDidChange, viewLayoutMarginsDidChange.

  • Для адаптивной верстки важны updateViewConstraints, intrinsicContentSize и UILayoutGuide.

Комментарии (3)


  1. rusl002
    04.09.2025 07:54

    Поясните пожалуйста, как didReceiveMemoryWarning официально устарел, если в документации сказано, что настоятельно рекомендуется его реализовать


    1. Dinozavr2005 Автор
      04.09.2025 07:54

      Спасибо за замечание,
      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 или работающих с действительно массивными кэшами.
      Вы правы, я некорректно выразился в статье, и формулировки лучше поправить, чтобы не вводить читателей в заблуждение. Корректнее будет сказать, что метод не устарел официально.


      1. spiceginger
        04.09.2025 07:54

        Даже не совсем так. WWDC сессия по управлению памятью рекомендует НЕ освобождать данные в массивах и тд потому что система использует сжатие памяти и если начать трогать объекты в массивах(и тд) даже для того что бы удалить это может привести к увеличению потребления памяти так как системе придется разжать эту область памяти. Поэтому рекомендуется на время например перестать что то кэшировать и тд и обнулить ссылки на объекты которые больше не нужны и могут быть легко восстановлены (Это относится и к структурам данных, обнулить их можно. Итерироваться что б найти например не нужные данные не стоит).