На первый взгляд,
onAppear кажется простым и понятным: показалась вью - сработало, скрылась - сработало
onDisappear. Но на практике все сложнее. В зависимости от того, где находится вью - в
TabView,
NavigationStack или
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 вдруг не сбрасывается или, наоборот, сбрасывается не вовремя. Разные контейнеры ведут себя по-разному и это нужно тестировать, а не гадать.