Асинхронность: async/await и сеть
Реальные приложения загружают данные из интернета. Swift делает асинхронный код читаемым с помощью async/await — он выглядит почти как обычный последовательный код.
Суть урока: async помечает функцию, которая может приостановиться (например, ждать ответа сети), а await отмечает точку ожидания. Код читается сверху вниз, но не блокирует интерфейс.
Загрузка данных занимает время, и блокировать интерфейс на это время нельзя — приложение «зависнет». Раньше для этого писали запутанные колбэки. Swift предлагает async/await: асинхронная функция помечается async, а места ожидания — словом await:
struct Post: Decodable, Identifiable {
let id: Int
let title: String
}
func loadPosts() async throws -> [Post] {
let url = URL(string: "https://api.example.com/posts")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([Post].self, from: data)
}Читается линейно: получить данные, дождаться ответа, декодировать. Но под капотом на await функция приостанавливается, освобождая поток, и возобновляется, когда данные пришли. Интерфейс при этом остаётся отзывчивым. Запускать асинхронную работу из вью удобно модификатором .task — он автоматически стартует при появлении вью и отменяется при исчезновении:
struct FeedView: View {
@State private var posts: [Post] = []
var body: some View {
List(posts) { post in Text(post.title) }
.task {
posts = (try? await loadPosts()) ?? []
}
}
}Обновление интерфейса должно происходить в главном потоке. SwiftUI-вью уже работают на @MainActor, поэтому присваивание posts безопасно. Современный совет 2024-2025: держите UI-код на @MainActor, а тяжёлые вычисления выносите в фон. Для параллельных задач есть async let и TaskGroup, образующие структурированную конкурентность — задачи живут в предсказуемой иерархии и не «теряются».
Главный поток (UI) Сеть | .task стартует | await loadPosts() ----------+ | (поток свободен, UI живёт) | загрузка... | | | <---- данные пришли ---------+ | posts = [...] (на @MainActor) v список обновлён
Попробуй сам ▶ — запусти код прямо в браузере (Pyodide). Здесь нет Swift, но логика та же, что под капотом мобильного кода:
# Имитируем async/await: ожидание без блокировки через asyncio.
import asyncio
async def load_posts():
await asyncio.sleep(0) # как await на сети — точка приостановки
return [{'id': 1, 'title': 'Swift 6'},
{'id': 2, 'title': 'SwiftUI'}]
async def main():
posts = await load_posts() # дождались, не блокируя
for p in posts:
print(p['id'], p['title'])
asyncio.run(main())Как работает под капотом
Когда выполнение доходит до await, функция может приостановиться (suspend): её состояние сохраняется, поток освобождается для другой работы, а при готовности результата функция возобновляется — возможно, уже на другом потоке. Актор @MainActor гарантирует, что отмеченный им код всегда исполняется на главном потоке, защищая UI-состояние от гонок. Структурированная конкурентность связывает дочерние задачи с родительской: если родитель отменён, дети тоже отменяются, что исключает «осиротевшие» задачи и утечки.
Частые ошибки
- Обновлять UI из фонового потока. Это приводит к гонкам; UI-код должен быть на @MainActor.
- Запускать сетевой запрос напрямую в body. Используйте .task, а не побочные эффекты в body.
- Игнорировать ошибки. Сеть ненадёжна — обрабатывайте throws через try/catch.
Best practices
- Запускайте загрузку через .task — она сама стартует и отменяется вместе с вью.
- Держите UI на @MainActor, тяжёлые вычисления выносите в фоновые задачи.
- Для параллельных независимых запросов используйте async let.
Итоги. async/await делает асинхронный код линейным и читаемым, не блокируя интерфейс. Модификатор .task связывает загрузку с жизнью вью, @MainActor защищает UI, а структурированная конкурентность держит задачи под контролем. Это современный стандарт работы с данными в iOS.
Шире контекста
Появление async/await изменило мобильную разработку так же, как когда-то опционалы. До него асинхронный код тонул в вложенных колбэках — «пирамиде гибели», где обработка ошибок и порядок выполнения становились нечитаемыми. Теперь асинхронный код выглядит почти как обычный последовательный, но при этом не блокирует интерфейс. Вокруг этого выстроена целая модель: акторы изолируют изменяемое состояние и исключают гонки данных, @MainActor держит UI на главном потоке, структурированная конкурентность с async let и TaskGroup связывает дочерние задачи с родителем, чтобы ни одна не потерялась и не утекла. Современные рекомендации 2024-2025 годов советуют по умолчанию держать код на главном акторе и выносить в фон только тяжёлые вычисления — это проще и безопаснее, чем вручную жонглировать потоками. Строгий режим Swift 6 доводит идею до предела, превращая потенциальные гонки данных в ошибки компиляции, и хотя поначалу это строго, в итоге вы получаете асинхронный код, которому можно доверять.