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

Ответ

Оба метода вызываются в процессе лайаута (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()              // Если появляется заново

Ответ 18+ 🔞

Давай разберём эту парочку, а то как-то всё сухо и академично. Смотри, есть два метода, которые вызываются, когда система пересчитывает, где и какого размера должны быть все твои вьюхи на экране. Это как будто тебе говорят: "Слушай, щас всё переставим, готовься!" и потом "Всё, переставили, проверяй!".

viewWillLayoutSubviews() — это как крик: "Внимание, щас начнём двигать всё нахуй!". Вызывается до того, как система посчитает новые frame и bounds для всех subviews. В этот момент у тебя ещё старые, текущие размеры.

viewDidLayoutSubviews() — это отмашка: "Всё, блядь, расставили!". Вызывается после того, как система всё посчитала и применила. Теперь у всех твоих вьюх — свежие, актуальные размеры. Идеально, чтобы подправить что-то в последний момент.

Короче, в чём разница:

Штука viewWillLayoutSubviews() viewDidLayoutSubviews()
Когда зовут Перед тем как всё посчитать После того как всё посчитали и применили
Какие bounds Старые, ещё не поменялись Новые, уже всё обновилось
Зачем обычно Сохранить что было, подготовиться Подправить интерфейс под итоговые размеры
Сколько раз дергают Много, при любом чихе Много, при любом чихе
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

        // Можно сказать системе: "эй, пересчитай констрейнты, я тут новые приготовил"
        view.setNeedsUpdateConstraints()
    }

    // 2. viewDidLayoutSubviews - всё, отбой, можно править
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews() // Опять не забудь!

        // Теперь все subviews имеют свои настоящие, новые размеры
        let safeAreaTop = view.safeAreaInsets.top

        // Делаем шапку круглой, зная её итоговую высоту
        headerView.layer.cornerRadius = headerView.bounds.height / 2

        // Возвращаем скролл на то же место, где он был до поворота
        scrollView.contentOffset = previousScrollOffset

        // Запускаем свою кастомную логику расстановки
        updateCustomLayout()
    }

    private func updateCustomLayout() {
        // Тут уже можно спокойно работать с финальными размерами
        let headerHeight = headerView.bounds.height
        // ... дальше твоя магия
    }
}

Когда что юзать, чтобы не вышло пиздеца:

Юзай viewWillLayoutSubviews для:

  • Сохранения того, что было (позиция скролла, какая ячейка выделена).
  • Подготовки данных для нового лейаута.
  • Чтобы крикнуть системе: "пересчитай констрейнты!".
  • Почистить какой-нибудь кэш, который зависит от размеров.

Юзай viewDidLayoutSubviews для:

  • Подгонки интерфейса под итоговые размеры (скругления, тени).
  • Настройки слоёв (CALayer) — они любят точные размеры.
  • Обновления своей кастомной логики расстановки.
  • Восстановления состояния после поворота или смены трейтов.
  • Вызова методов, которым нужны уже актуальные frame и bounds.

Важные предупреждения, а то накосячишь:

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

Как это обычно происходит при повороте экрана:

1. viewWillTransition(to:with:)  // Начинаем поворачиваться
2. viewWillLayoutSubviews()      // Внимание! Сейчас всё поедет!
3. traitCollectionDidChange()    // Если поменялись трейты (размер класс)
4. viewDidLayoutSubviews()       // Всё, отбой, расставили по-новому
5. viewWillAppear()              // Если вьюха снова появляется