Введение
Современные iOS и macOS приложения редко обходятся без взаимодействия с удалёнными серверами. Будь то загрузка данных из новостной ленты, отправка формы регистрации или синхронизация файлов — все эти операции требуют грамотной реализации HTTP клиента. В экосистеме Swift разработчику доступен мощный встроенный фреймворк URLSession, который с каждым годом становится всё удобнее благодаря внедрению async/await.
В этой статье мы разберём, как правильно создавать HTTP запросы на Swift, начиная с базовых структур и заканчивая продвинутыми техниками обработки ошибок и сериализации данных. Вы узнаете, как писать чистый, безопасный и производительный сетевой код для ваших приложений.
Основы URLSession: первый HTTP запрос
Класс URLSession является фундаментом для любой сетевой работы в Swift. Он управляет конфигурацией, кэшированием, сессиями и непосредственно выполнением запросов. Рассмотрим простейший GET-запрос для получения данных с сервера.
Создание простого GET-запроса
import Foundation
// 1. Создаём URL
guard let url = URL(string: "https://api.example.com/users") else {
print("Неверный URL")
return
}
// 2. Создаём запрос (по умолчанию GET)
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("application/json", forHTTPHeaderField: "Accept")
// 3. Создаём сессию
let session = URLSession.shared
// 4. Выполняем запрос (старый способ с замыканием)
let task = session.dataTask(with: request) { data, response, error in
if let error = error {
print("Ошибка запроса: \(error.localizedDescription)")
return
}
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
print("Сервер вернул ошибку")
return
}
if let data = data,
let jsonString = String(data: data, encoding: .utf8) {
print("Полученные данные: \(jsonString)")
}
}
task.resume() // Важно! Запускаем задачу
Ключевые моменты: всегда проверяйте статус ответа (200...299), не забывайте вызывать resume(), и обрабатывайте возможные ошибки. В реальных проектах этот код обычно выносят в отдельный сервисный слой.
Современный подход: async/await
Начиная с Swift 5.5, появилась возможность использовать async/await, что делает код гораздо читабельнее и избавляет от «пирамиды замыканий».
func fetchUsers() async throws -> Data {
guard let url = URL(string: "https://api.example.com/users") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("application/json", forHTTPHeaderField: "Accept")
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
}
Этот код можно вызывать из любой асинхронной функции или задачи, что значительно упрощает логику.
Работа с JSON: Codable и модели данных
Редко когда данные приходят в «сыром» виде. Чаще всего мы имеем дело с JSON. Swift предлагает элегантное решение — протокол Codable (объединяющий Encodable и Decodable).
Создание модели и декодирование
// 1. Определяем структуру данных
struct User: Codable {
let id: Int
let name: String
let email: String
}
// 2. Асинхронная функция для получения массива пользователей
func fetchUsers() async throws -> [User] {
let data = try await fetchUsersData() // из предыдущего примера
let decoder = JSONDecoder()
return try decoder.decode([User].self, from: data)
}
Отправка POST-запроса с телом
Для отправки данных на сервер (например, создание нового пользователя) используем POST-запрос с кодированием объекта.
func createUser(_ user: User) async throws -> User {
guard let url = URL(string: "https://api.example.com/users") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("application/json", forHTTPHeaderField: "Accept")
// Кодируем объект в JSON
let encoder = JSONEncoder()
request.httpBody = try encoder.encode(user)
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 try JSONDecoder().decode(User.self, from: data)
}
Обратите внимание: мы всегда устанавливаем заголовки Content-Type и Accept, чтобы сервер правильно интерпретировал наши данные.
Обработка ошибок и продвинутые техники
Надёжный HTTP клиент должен не просто отправлять запросы, но и корректно обрабатывать все возможные сценарии: таймауты, сетевые ошибки, неверные статус-коды.
Создание собственных типов ошибок
enum NetworkError: LocalizedError {
case invalidURL
case invalidResponse
case httpError(statusCode: Int)
case decodingError(Error)
var errorDescription: String? {
switch self {
case .invalidURL:
return "Неверный URL"
case .invalidResponse:
return "Сервер вернул некорректный ответ"
case .httpError(let code):
return "HTTP ошибка: \(code)"
case .decodingError(let error):
return "Ошибка декодирования: \(error.localizedDescription)"
}
}
}
// Использование в функции
func fetchUser(id: Int) async throws -> User {
guard let url = URL(string: "https://api.example.com/users/\(id)") else {
throw NetworkError.invalidURL
}
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.invalidResponse
}
guard (200...299).contains(httpResponse.statusCode) else {
throw NetworkError.httpError(statusCode: httpResponse.statusCode)
}
do {
return try JSONDecoder().decode(User.self, from: data)
} catch {
throw NetworkError.decodingError(error)
}
}