HTTP клиент на Swift: делаем запросы правильно от А до Я

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение

Современные 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)
    }
}

Настройка таймаутов и кэ

Блоги

Рекомендаций по книгам