Многие разработчики привыкли вызывать
API прямо в
.onAppear. Вроде логично: экран показался - пора грузить данные. В маленьких проектах это работает. Но когда приложение растет, начинаются проблемы: дублирующиеся запросы, утечки памяти, неправильное состояние интерфейса. Давайте разберемся, почему
.onAppear - это ловушка и как правильно выполнять загрузку данных с помощью
.task и конечного автомата.
В чем проблема .onAppear:
В
SwiftUI вью - это легковесные структуры. Они создаются и пересоздаются постоянно.
.onAppear срабатывает каждый раз, когда представление становится видимым. Если пользователь открыл экран, вернулся назад и снова зашел - запрос улетит повторно, хотя данные уже есть.
Еще хуже с отменой задач. Когда вы пишете
Task { await loadData() } внутри
.onAppear, вы создаете неструктурированную задачу. Если пользователь закрыл экран до завершения запроса, задача продолжает висеть в памяти. Результат - утечки, лишний трафик, проблемы с батареей.
Можно добавить ручную отмену в
.onDisappear, но это лишний код, который легко забыть. А еще классическая проблема - куча флагов
isLoading,
error,
data. Они независимы, и интерфейс может перейти в противоречивое состояние: например, одновременно крутится лоадер и показывается ошибка.
Что использовать вместо этого:
Вместо трюков с флагами нужно ввести конечный автомат - перечисление, которое описывает все возможные состояния экрана:
- .idle - еще ничего не началось.
- .loading - идет загрузка.
- .loaded(User) - данные получены.
- .error(Error) - произошла ошибка.
Это гарантирует, что в каждый момент времени экран может находиться только в одном состоянии. Никакой путаницы. Бизнес-логику выносим в отдельную
ViewModel (класс с @Observable). В ней один метод, который меняет состояние.
Почему .task лучше:
В представлении вместо
.onAppear используем модификатор
.task потому что он:
- Работает с async/await из коробки - не нужно вкладывать задачу в Task { }.
- Автоматически отменяет запрос, если представление было удалено из иерархии.
- Не требует ручной отмены в .onDisappear.
Вот как выглядит финальный код:
enum ViewState {
case idle
case loading
case loaded(User)
case error(Error)
}
@Observable
class ProfileViewModel {
var state: ViewState = .idle
func fetchUser() async {
state = .loading
do {
let user = try await api.getUser()
state = .loaded(user)
} catch {
state = .error(error)
}
}
}
struct ProfileView: View {
@State private var viewModel = ProfileViewModel()
var body: some View {
Group {
switch viewModel.state {
case .idle: Color.clear
case .loading: ProgressView()
case .loaded(let user): Text(user.name)
case .error(let error): Text(error.localizedDescription)
}
}
.task {
await viewModel.fetchUser()
}
}
}
Вывод:
.onAppear - это не инструмент для загрузки данных. Это событие жизненного цикла. Используя его для
API, вы боретесь с фреймворком: вручную управляете отменой задач, плодите невозможные состояния и рискуете утечками. Правильный подход - конечный автомат на перечислениях и модификатор
.task, который берет на себя управление жизненным циклом. Код становится чище, а компилятор сам следит за тем, чтобы вы обработали все состояния.