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

Прекратите использовать .onAppear для вызовов запросов к API в SwiftUI

Многие разработчики привыкли вызывать 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, который берет на себя управление жизненным циклом. Код становится чище, а компилятор сам следит за тем, чтобы вы обработали все состояния.
03.06.2026 54 532