Async await в Swift: первый сетевой запрос

Async/await в Swift позволяет писать асинхронный код без леса callback. В этом уроке мы сделаем первый сетевой запрос через URLSession, декодируем JSON через JSONDecoder, обработаем ошибку через do/catch и покажем результат в SwiftUI

Цель — не построить production networking layer, а пройти понятный маршрут: модель данных, async-функция, Task, loading state, ошибка, вывод результата на экран

Что получится в конце

Модель:

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-кода

Когда сетевой пример не работает, проверяйте по слоям:

  1. Собрался ли URL
  2. Пришел ли HTTP-ответ
  3. Статус в диапазоне 200..<300
  4. Есть ли данные
  5. Совпадает ли Decodable-модель с JSON
  6. Обновляется ли 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

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x