Добавить объявление

Жизненный цикл SwiftUI View: когда onAppear вызывается не так, как ожидали

На первый взгляд, onAppear кажется простым и понятным: показалась вью - сработало, скрылась - сработало onDisappear. Но на практике все сложнее. В зависимости от того, где находится вью - в TabViewNavigationStack или List - поведение может кардинально отличаться. И если не понимать этих нюансов, можно нарваться на баги, которые трудно воспроизвести.

Две разные системы:


Чтобы понять, что происходит, нужно разделить два понятия: время жизни узла (node lifetime) и видимость на экране (visibility). Узел живет, пока существует идентичность вью. К узлу привязаны @State и @StateObject. А видимость - это то, видит ли пользователь вью сейчас. onAppear и onDisappear реагируют именно на видимость, а не на создание или уничтожение узла.

В простых случаях эти две шкалы совпадают. Но в сложных контейнерах - расходятся.

TabView - узлы живут, видимость меняется:


Когда вы переключаете вкладки, узлы вью не уничтожаются. Они остаются в памяти, меняется только флаг видимости. Поэтому onAppear срабатывает при каждом переключении на вкладку, но @State сохраняет свое значение.

Нюанс: на iOS 17 TabView создавал все вкладки сразу при запуске. На iOS 18+ - только при первом открытии. Один и тот же код может вести себя по-разному на разных версиях.

NavigationStack - при возврате узел уничтожается:


В навигационном стеке при возврате назад (pop) узел детальной вью уничтожается, а @State сбрасывается. При повторном переходе создается новый узел. В отличие от TabView, здесь данные не сохраняются между показами.

List и LazyVStack - узлы создаются и уничтожаются при скролле:


В ленивых списках узлы для ячеек создаются, когда те подъезжают к видимой области, и могут быть уничтожены, когда уезжают далеко. Поэтому @State внутри ячейки нельзя использовать для хранения данных - при повторном появлении оно сбросится.

Главное правило - body выполняется до onAppear:


body вычисляется до того, как сработает onAppear или .task. Нельзя полагаться на то, что в onAppear данные подгрузятся до первого вызова body. Нужно всегда предусматривать начальное состояние.

Как защитить свой код:


  • Всегда проверяйте прежде чем выполнять новые запросы в .task, ведь загрузка уже может выполняться.

  • Используйте флаг hasLoaded, чтобы отличать первую загрузку от возвращения на вкладку.

  • Добавляйте print в onAppear, onDisappear и .task, чтобы видеть реальную картину.

  • Помните, что у разных контейнеров свои правила жизни узлов.

Вывод:


onAppear в SwiftUI - это не «вью создана», а «вью стала видимой». Время жизни узла и видимость - это две независимые шкалы. Понимание этого спасает от багов, когда @State вдруг не сбрасывается или, наоборот, сбрасывается не вовремя. Разные контейнеры ведут себя по-разному и это нужно тестировать, а не гадать.
01.05.2026 54 385