Вопрос проверяет понимание жизненного цикла экрана, правил обновления UI и безопасной асинхронности в UIKit.
Нужно запускать запрос так, чтобы он был связан с жизненным циклом экрана и мог быть отменен. UI обновляется только на главном потоке. Ошибки и состояния загрузки должны обрабатываться явно. Важно избегать retain cycle и обновления UI после закрытия экрана. Лучше держать сетевую логику вне контроллера, а контроллеру оставлять только отображение состояния.
Асинхронные запросы в контроллере опасны тем, что сеть живет «дольше», чем экран. Поэтому правильный подход — связать запрос с жизненным циклом и держать управление состоянием прозрачным.
Перед кодом важно зафиксировать правила, которые должны выполняться всегда.
UI обновляется только на main thread.
Запрос должен быть отменяемым.
Результат запроса не должен применяться, если экран больше не актуален.
Не должно быть утечек памяти из-за self в замыканиях или задачах.
Состояния loading/success/error должны быть явными.
Обычно хранят ссылку на задачу и отменяют при уходе с экрана.
final class FeedViewController: UIViewController {
private var loadTask: Task<Void, Never>?
override func viewDidLoad() {
super.viewDidLoad()
load()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
loadTask?.cancel()
}
private func load() {
setLoading(true)
loadTask = Task { [weak self] in
guard let self else { return }
do {
let items = try await self.service.fetchFeed() // service скрывает URLSession
await MainActor.run {
self.setLoading(false)
self.render(items)
}
} catch {
await MainActor.run {
self.setLoading(false)
self.showError(error)
}
}
}
}
}
Ключевые детали:
loadTask хранится, чтобы отменить.
Task { [weak self] ... } снижает риск retain cycle.
UI обновляется внутри MainActor.run.
Если не отменять:
запрос продолжится;
результат может прийти после pop/dismiss;
возможны лишние обновления, утечки, гонки состояния.
Частая проблема: пользователь дергает pull-to-refresh, а старый запрос еще идет.
Практики:
отменять предыдущую задачу перед запуском новой;
хранить текущее состояние загрузки;
игнорировать устаревшие ответы (например, по requestId).
Идея:
loadTask?.cancel()
loadTask = Task { ... }
Контроллер должен уметь «отрисовать» состояние одним способом, а не разрозненно.
Подход:
завести ScreenState;
обновлять его централизованно;
иметь один метод render(state:).
Даже без полного MVVM это резко снижает хаос в контроллере.
Правильное разделение обычно выглядит так:
Контроллер:
запускает загрузку;
отображает состояния;
обрабатывает пользовательские действия.
Сервис/репозиторий:
выполняет запросы;
декодирует данные;
возвращает модели или доменные ошибки.
Если декодирование и построение моделей происходит внутри контроллера — это почти всегда признак будущего Massive VC.
Корректная обработка асинхронных запросов в view controller строится на трех вещах: отмена и связь с жизненным циклом, обновление UI только на main thread, явное управление состояниями (loading/success/error). Чем больше логики сети и маппинга вынесено из контроллера, тем стабильнее и поддерживаемее экран.