$ sudo teach IT

Почти любое настоящее приложение в какой-то момент обращается к серверу: получить список товаров, отправить форму, проверить логин и пароль. До сих пор все данные в примерах жили прямо в коде или приходили из готовой строки JSON. Сейчас разберёмся, как эти данные попадают в программу по-настоящему — через сеть.

Здесь пригодится то, что вы уже умеете: опционалы для необязательных значений, do/catch для ошибок, Codable для разбора JSON и, конечно, async/await — сетевой запрос почти всегда асинхронный, ведь ответ сервера приходит не мгновенно.

Важная оговорка про песочницу

Песочница курса, где вы решаете задачи, не имеет доступа к интернету — из соображений безопасности сервер сайта не разрешает своему коду ходить куда-то наружу. Поэтому в этом уроке нет задач с автопроверкой: код, который делает настоящий сетевой запрос, здесь просто негде выполнить. Все примеры ниже нужно понимать и при желании пробовать на своей машине: в Xcode на Mac, либо через установленный Swift toolchain на Linux или Windows с обычным доступом в интернет. Синтаксис от платформы не зависит — URLSession работает одинаково везде.

URL и URLSession

Адрес, по которому обращаются к серверу, описывается типом URL из Foundation. Инициализатор URL(string:) — failable, он возвращает опционал, потому что не любая строка — корректный адрес:

let url = URL(string: "https://api.example.com/users/7")

Сами запросы выполняет URLSession — объект, который знает, как установить соединение, отправить запрос и получить ответ. Для большинства случаев достаточно готового общего экземпляра URLSession.shared, отдельно его создавать не нужно.

GET-запрос с async/await

Самый простой способ получить данные — метод data(from:). Он приостанавливает выполнение до прихода ответа и возвращает пару значений: сырые байты и объект с информацией об ответе:

struct User: Decodable {
    let id: Int
    let name: String
}

func fetchUser(id: Int) async throws -> User {
    let url = URL(string: "https://api.example.com/users/\(id)")!
    let (data, response) = try await URLSession.shared.data(from: url)

    guard let httpResponse = response as? HTTPURLResponse,
          httpResponse.statusCode == 200 else {
        throw URLError(.badServerResponse)
    }

    return try JSONDecoder().decode(User.self, from: data)
}

По шагам: функция помечена async throws — она может и приостанавливаться, и завершаться ошибкой. await перед вызовом data(from:) — та самая точка приостановки: пока сервер не ответил, выполнение этой конкретной задачи стоит на паузе, а не блокирует всю программу. Дальше response проверяется через приведение типа к HTTPURLResponse — именно в нём лежит код ответа. И только если код равен 200, данные передаются в уже знакомый JSONDecoder.

Код ответа — не ошибка сама по себе

Здесь кроется важная тонкость: URLSession выбрасывает ошибку только при сбое самого соединения — нет сети, сервер недоступен, истекло время ожидания. Если сервер честно ответил, но с кодом 404 или 500, метод data(from:) всё равно вернёт данные и не выбросит исключение — тело ответа может содержать, например, страницу с описанием ошибки. Проверять успешность нужно вручную, глядя на statusCode:

КодЧто означает
200OK — запрос выполнен успешно
201Created — запись успешно создана (обычно после POST)
404Not Found — такого адреса или записи не существует
500Internal Server Error — сервер сломался на своей стороне

Диапазон допустимых кодов часто проверяют через Range, а не через сравнение с одним числом, ведь «успех» — это весь диапазон 200–299:

guard let httpResponse = response as? HTTPURLResponse,
      (200...299).contains(httpResponse.statusCode) else {
    throw URLError(.badServerResponse)
}

POST-запрос: отправка данных

Метод data(from:) умеет только простой GET и не позволяет задать метод запроса, заголовки или тело. Для этого используется URLRequest — описание запроса, которое затем передаётся в метод data(for:):

struct NewComment: Encodable {
    let text: String
}

func postComment(_ text: String) async throws -> Data {
    let url = URL(string: "https://api.example.com/comments")!
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpBody = try JSONEncoder().encode(NewComment(text: text))

    let (data, response) = try await URLSession.shared.data(for: request)

    guard let httpResponse = response as? HTTPURLResponse,
          (200...299).contains(httpResponse.statusCode) else {
        throw URLError(.badServerResponse)
    }

    return data
}

request объявлен через var, потому что после создания в него дописываются метод, заголовок и тело. httpBody — это уже привычный JSONEncoder, превращающий структуру, соответствующую Encodable, в байты. Заголовок Content-Type подсказывает серверу, что в теле запроса лежит именно JSON.

Обработка ошибок сети

Ошибки соединения — тип URLError, соответствующий протоколу Error. У него есть свойство code с конкретной причиной, поэтому в catch удобно сопоставлять по условию where:

func loadUserSafely(id: Int) async {
    do {
        let user = try await fetchUser(id: id)
        print("Загружен пользователь: \(user.name)")
    } catch let error as URLError where error.code == .notConnectedToInternet {
        print("Нет подключения к интернету")
    } catch let error as URLError where error.code == .timedOut {
        print("Сервер не ответил вовремя")
    } catch {
        print("Не удалось загрузить пользователя: \(error)")
    }
}

Такое разделение важно для пользователя приложения: «нет интернета» и «сервер сломан» — разные ситуации, и сообщение стоит показывать разное, а не одно общее «что-то пошло не так».

Вызывается всё это, как и любая асинхронная функция, изнутри Task или другой async-функции:

Task {
    await loadUserSafely(id: 7)
}

Частые ошибки

  • Считать, что раз ошибки не было, значит запрос точно успешный — код ответа нужно проверять отдельно, даже без выброшенного исключения.
  • Использовать data(from:) там, где нужен POST с заголовками и телом — для этого существует именно data(for:) с URLRequest.
  • Забывать про await и пытаться вызвать сетевой метод как обычную функцию — компилятор такого не пропустит вне async-контекста.
  • Принудительно разворачивать URL(string:) оператором ! для адреса, который приходит не как константа в коде, а откуда-то извне — если строка окажется некорректной, программа упадёт.
  • Пытаться проверить такой код в песочнице курса — она намеренно не имеет доступа к сети, для проверки нужен компьютер с настоящим интернетом.

Резюме

  • URLSession.shared.data(from:) выполняет GET-запрос по URL и возвращает данные вместе с ответом сервера.
  • Успешность запроса проверяется через HTTPURLResponse.statusCode — отсутствие исключения ещё не значит успех.
  • Для POST и других методов с телом и заголовками используется URLRequest и метод data(for:).
  • Тело запроса и ответа кодируется и декодируется уже знакомыми JSONEncoder и JSONDecoder.
  • URLError различает причины сбоя соединения — можно показывать пользователю осмысленное сообщение вместо общей ошибки.
  • Этот код нельзя проверить в песочнице курса — пробуйте его на своей машине с доступом в интернет.

Проверьте себя

3 вопроса