Async/await в Swift позволяет писать асинхронный код без леса callback. В этом уроке мы сделаем первый сетевой запрос через URLSession, декодируем JSON через JSONDecoder, обработаем ошибку через do/catch и покажем результат в SwiftUI
Цель — не построить production networking layer, а пройти понятный маршрут: модель данных, async-функция, Task, loading state, ошибка, вывод результата на экран
- Что получится в конце
- Что такое async throws
- URLSession.data
- JSONDecoder и Decodable
- SwiftUI-экран
- Loading state
- Проверка HTTP-статуса
- Домашка: загрузить другой объект
- Частые ошибки и порядок проверки
- Разделяем загрузку и отображение
- Показываем разные состояния UI
- Почему проверка status code важнее, чем кажется
- Мини-план для следующего сетевого урока
- Как не потеряться в ошибках async-кода
- Домашка: сообщение об успешной загрузке
- Что может быть еще интересно по этой теме
- Что почитать дальше по Swift
Что получится в конце
Модель:
struct Todo: Decodable {
let id: Int
let title: String
let completed: Bool
}
Загрузка:
func loadTodo() async throws -> Todo {
let url = URL(string: "https://jsonplaceholder.typicode.com/todos/1")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(Todo.self, from: data)
}
Вызов из SwiftUI:
Task {
do {
todo = try await loadTodo()
} catch {
message = "Failed: \(error.localizedDescription)"
}
}
После нажатия кнопки экран покажет заголовок задачи или ошибку
Что такое async throws
Функция:
func loadTodo() async throws -> Todo
говорит две вещи. async означает, что функция выполняется асинхронно и вызывается через await. throws означает, что она может бросить ошибку
Вызов:
let todo = try await loadTodo()
читается как: дождаться асинхронного результата и быть готовым к ошибке
Если забыть try, компилятор напомнит про throws. Если забыть await, он напомнит про async. Это не шум, а защита от скрытых асинхронных операций
URLSession.data
Запрос:
let (data, response) = try await URLSession.shared.data(from: url)
возвращает байты ответа и response. В первом примере можно не использовать response, но в реальном коде полезно проверить HTTP status code
Упрощенный вариант:
let (data, _) = try await URLSession.shared.data(from: url)
Подчеркивание означает: второе значение получаем, но не используем
JSONDecoder и Decodable
Чтобы декодировать JSON, модель должна соответствовать Decodable:
struct Todo: Decodable {
let id: Int
let title: String
let completed: Bool
}
Декодирование:
try JSONDecoder().decode(Todo.self, from: data)
Если JSON не совпадает с моделью, декодер бросит ошибку. Это хорошо: вы узнаете, что форма данных не та, которую ожидает приложение
SwiftUI-экран
Минимальный экран:
import SwiftUI
struct ContentView: View {
@State private var todo: Todo?
@State private var message = "Not loaded"
var body: some View {
VStack(spacing: 16) {
Text(todo?.title ?? message)
Button("Load") {
Task {
do {
todo = try await loadTodo()
message = "Loaded"
} catch {
message = "Failed: \(error.localizedDescription)"
}
}
}
}
.padding()
}
}
Task запускает async-код из синхронного button action. Без Task вы не можете просто написать await внутри обычного обработчика кнопки
Loading state
Хороший экран должен показывать ожидание:
@State private var isLoading = false
Внутри Task:
isLoading = true
defer { isLoading = false }
В UI:
if isLoading {
ProgressView()
}
Даже в учебном примере loading state важен. Пользователь должен видеть, что приложение работает, а не зависло
Проверка HTTP-статуса
Более аккуратная версия:
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode else {
throw URLError(.badServerResponse)
}
Теперь код не считает любой ответ успешным. Это важно для реальных API: сервер может вернуть HTML-ошибку, 404 или 500, а декодер потом покажет неочевидную ошибку JSON
Домашка: загрузить другой объект
Поменяйте URL:
https://jsonplaceholder.typicode.com/users/1
Создайте модель:
struct User: Decodable {
let id: Int
let name: String
let email: String
}
Выведите имя и email. Дополнительная задача: добавьте loading state и отдельное сообщение для ошибки
Частые ошибки и порядок проверки
Cannot pass function of type async to synchronous context Вы пытаетесь вызвать async-код там, где нельзя использовать await. В button action используйте Task
Забыли try или await Если функция async throws, вызов почти всегда выглядит как try await
JSON не декодируется Проверьте, совпадают ли имена и типы полей модели с JSON
Не проверяете HTTP status Для первого примера можно упростить, но в реальном коде проверяйте HTTPURLResponse
Разделяем загрузку и отображение
Даже в первом примере полезно не смешивать сетевой код и разметку. loadTodo() уже вынесен в отдельную функцию, поэтому экран отвечает за состояние, а функция отвечает за получение данных
Можно сделать еще яснее:
@State private var todo: Todo?
@State private var isLoading = false
@State private var errorMessage: String?
И отдельный метод внутри view:
func load() {
Task {
isLoading = true
errorMessage = nil
do {
todo = try await loadTodo()
} catch {
errorMessage = error.localizedDescription
}
isLoading = false
}
}
Для учебного экрана это нормально. Вы видите все состояния рядом: пусто, загрузка, успех, ошибка. Позже этот код можно вынести в view model, но сначала важно понять сам цикл
Показываем разные состояния UI
Экран должен честно показывать, что происходит:
if isLoading {
ProgressView("Loading")
} else if let todo {
VStack {
Text(todo.title)
Text(todo.completed ? "Done" : "Open")
}
} else if let errorMessage {
Text(errorMessage)
.foregroundStyle(.red)
} else {
Text("Press Load")
}
Это выглядит длиннее, чем один Text, зато пользователь понимает состояние приложения. Для SEO-урока это тоже важная логика: читатель должен увидеть не только счастливый путь, но и нормальное поведение при ошибке
Почему проверка status code важнее, чем кажется
URLSession может успешно получить ответ сервера, даже если сервер вернул 404 или 500. С точки зрения сети запрос состоялся. Но с точки зрения приложения это ошибка бизнес-логики или API
Поэтому проверка:
guard let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode else {
throw URLError(.badServerResponse)
}
защищает от ситуации, когда вы пытаетесь декодировать страницу ошибки как JSON-модель Todo. Без этой проверки сообщение декодера может увести новичка не туда: он будет чинить модель, хотя проблема в ответе сервера
Мини-план для следующего сетевого урока
После одиночного Todo логичный следующий шаг — список:
struct Todo: Identifiable, Decodable {
let id: Int
let title: String
let completed: Bool
}
Функция будет возвращать массив:
func loadTodos() async throws -> [Todo] {
let url = URL(string: "https://jsonplaceholder.typicode.com/todos")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([Todo].self, from: data)
}
А экран сможет показать List(todos). Но не переходите к списку, пока одиночный объект не стал понятен. В сетевом коде лучше двигаться маленькими проверяемыми шагами
Как не потеряться в ошибках async-кода
Когда сетевой пример не работает, проверяйте по слоям:
- Собрался ли
URL - Пришел ли HTTP-ответ
- Статус в диапазоне
200..<300 - Есть ли данные
- Совпадает ли
Decodable-модель с JSON - Обновляется ли SwiftUI-состояние после ответа
Не начинайте сразу с UI. Если модель не декодируется, экран не виноват. Если URL неверный, JSONDecoder тоже не виноват. Такой порядок проверки экономит много времени
Домашка: сообщение об успешной загрузке
Добавьте отдельное состояние:
@State private var lastLoadedAt: Date?
После успешной загрузки:
lastLoadedAt = Date()
И покажите текст:
if let lastLoadedAt {
Text("Loaded at \(lastLoadedAt.formatted())")
}
Так вы увидите, что async-запрос может обновлять не одно значение, а несколько частей состояния экрана
Что может быть еще интересно по этой теме
Async/await заменяет URLSession? Нет. URLSession делает сетевой запрос, async/await дает удобный способ дождаться результата
Нужно ли делать networking в View? Для первого урока можно. В реальном проекте загрузку обычно выносят в отдельный слой или view model
Что делать с API keys? Не храните секреты прямо в открытом коде. В первом уроке используйте публичный учебный API без ключа
Что дальше после первого запроса? Состояния экрана, view model, список результатов и обработка ошибок. Но сначала важно понять Task, try await, URLSession и Decodable
Что почитать дальше по Swift
- SwiftUI: первый экран с Text, Button и @State
- @Binding и передача данных между SwiftUI views
- Optionals в Swift: ?, ! и безопасное извлечение
- Массивы, словари и циклы в Swift



