В чем разница между viewWillLayoutSubviews и viewDidLayoutSubviews в UIKit?

«В чем разница между viewWillLayoutSubviews и viewDidLayoutSubviews в UIKit?» — вопрос из категории UIKit, который задают на 10% собеседований IOS Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Оба метода вызываются в процессе лайаута (layout) представления, но в разные моменты цикла обновления.

viewWillLayoutSubviews() — вызывается перед тем, как система обновит геометрию (frame/bounds) всех subviews. Подходит для подготовки к изменениям.

viewDidLayoutSubviews() — вызывается после того, как система обновила геометрию всех subviews. Идеален для финальных корректировок, основанных на итоговых размерах.

Ключевые отличия:

Аспект viewWillLayoutSubviews() viewDidLayoutSubviews()
Время вызова Перед вычислением layout После вычисления layout
Состояние bounds Текущее (старое) значение Обновлённое (новое) значение
Типичное использование Сохранение состояния, подготовка Финальные корректировки UI
Количество вызовов Многократно при изменениях Многократно при изменениях
Вызов super Обязателен Обязателен

Практические примеры:

class CustomViewController: UIViewController {
    @IBOutlet weak var headerView: UIView!
    @IBOutlet weak var scrollView: UIScrollView!

    private var previousScrollOffset: CGPoint = .zero

    // 1. viewWillLayoutSubviews - подготовка
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()

        // Сохраняем текущее состояние ДО изменений
        previousScrollOffset = scrollView.contentOffset

        // Можно принудительно обновить constraints
        view.setNeedsUpdateConstraints()
    }

    // 2. viewDidLayoutSubviews - финальная настройка
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        // Все subviews имеют актуальные frame/bounds
        let safeAreaTop = view.safeAreaInsets.top

        // Корректируем UI на основе итоговых размеров
        headerView.layer.cornerRadius = headerView.bounds.height / 2

        // Восстанавливаем scroll position после rotation
        scrollView.contentOffset = previousScrollOffset

        // Обновляем custom layout
        updateCustomLayout()
    }

    private func updateCustomLayout() {
        // Работа с актуальными geometry
        let headerHeight = headerView.bounds.height
        // ... custom layout logic
    }
}

Когда что использовать:

Используйте viewWillLayoutSubviews для:

  • Сохранения текущего состояния UI (позиция скролла, выделение)
  • Подготовки данных для нового layout
  • Принудительного обновления constraints (setNeedsUpdateConstraints())
  • Очистки кэшей, зависящих от размеров

Используйте viewDidLayoutSubviews для:

  • Корректировки UI на основе итоговых размеров
  • Настройки layer properties (cornerRadius, shadows)
  • Обновления custom layout-логики
  • Восстановления состояния после изменений (rotation, trait collection)
  • Вызова методов, требующих актуальных geometry

Важные предупреждения:

  1. Оба метода вызываются многократно — при rotation, изменении constraints, вызове setNeedsLayout(), изменении safe area. Избегайте тяжёлых операций.
  2. Всегда вызывайте super — UIKit полагается на реализацию в родительских классах.
  3. Не изменяйте constraints в viewDidLayoutSubviews без необходимости — это может вызвать бесконечный цикл layout. Лучше делать это в viewWillLayoutSubviews или updateViewConstraints().
  4. Для однократной настройки используйте viewDidLoad() или viewWillAppear().

Типичный сценарий при rotation:

1. viewWillTransition(to:with:)  // Начало rotation
2. viewWillLayoutSubviews()      // Перед изменением layout
3. traitCollectionDidChange()    // Если изменились traits
4. viewDidLayoutSubviews()       // После изменения layout
5. viewWillAppear()              // Если появляется заново