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

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

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

Начать курс

Введение: почему правильный HTTP клиент важен для вашего Swift-приложения

Современные iOS и macOS приложения редко обходятся без взаимодействия с удалёнными серверами. Будь то загрузка данных из REST API, отправка аналитики или работа с облачными сервисами — HTTP клиент на Swift является фундаментом сетевого взаимодействия. Однако многие разработчики, особенно начинающие, допускают типичные ошибки: игнорируют обработку ошибок, не используют современные возможности языка (async/await) или создают «спагетти-код» из сетевых вызовов. В этой статье мы разберём, как написать надёжный и производительный HTTP клиент на Swift, следуя лучшим практикам.

1. Основы URLSession: сердце сетевых запросов

В стандартной библиотеке Swift за сетевое взаимодействие отвечает фреймворк Foundation и класс URLSession. Это мощный инструмент, который поддерживает конфигурацию сессий, кэширование, фоновые запросы и многое другое.

1.1. Создание базового HTTP клиента

Начнём с простой структуры, которая будет выполнять GET-запросы. Используем enum для описания возможных ошибок и протокол для гибкости:

import Foundation

enum HTTPClientError: Error {
    case invalidURL
    case noData
    case decodingError(Error)
    case networkError(Error)
    case statusCode(Int)
}

protocol HTTPClientProtocol {
    func get<T: Decodable>(from urlString: String, responseType: T.Type) async throws -> T
}

final class HTTPClient: HTTPClientProtocol {
    private let session: URLSession
    private let decoder: JSONDecoder
    
    init(session: URLSession = .shared, decoder: JSONDecoder = JSONDecoder()) {
        self.session = session
        self.decoder = decoder
    }
    
    func get<T: Decodable>(from urlString: String, responseType: T.Type) async throws -> T {
        guard let url = URL(string: urlString) else {
            throw HTTPClientError.invalidURL
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = "GET"
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        
        let (data, response) = try await session.data(for: request)
        
        guard let httpResponse = response as? HTTPURLResponse else {
            throw HTTPClientError.networkError(NSError(domain: "Invalid response", code: -1))
        }
        
        guard (200...299).contains(httpResponse.statusCode) else {
            throw HTTPClientError.statusCode(httpResponse.statusCode)
        }
        
        do {
            return try decoder.decode(T.self, from: data)
        } catch {
            throw HTTPClientError.decodingError(error)
        }
    }
}

1.2. Почему async/await?

До появления Swift 5.5 разработчики использовали замыкания (completion handlers) или сторонние библиотеки (Alamofire). Async/await делает код линейным, понятным и легко читаемым. Ошибки обрабатываются через do-catch, а не через вложенные замыкания.

2. POST, PUT, DELETE: работа с разными HTTP методами

Реальный HTTP клиент должен поддерживать все основные методы. Добавим в протокол методы для отправки данных и обновления ресурсов.

2.1. Расширение протокола для POST и PUT

extension HTTPClientProtocol {
    func post<T: Decodable, U: Encodable>(
        to urlString: String,
        body: U,
        responseType: T.Type
    ) async throws -> T {
        guard let url = URL(string: urlString) else {
            throw HTTPClientError.invalidURL
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        request.httpBody = try JSONEncoder().encode(body)
        
        return try await performRequest(request, responseType: responseType)
    }
    
    func put<T: Decodable, U: Encodable>(
        to urlString: String,
        body: U,
        responseType: T.Type
    ) async throws -> T {
        guard let url = URL(string: urlString) else {
            throw HTTPClientError.invalidURL
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = "PUT"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        request.httpBody = try JSONEncoder().encode(body)
        
        return try await performRequest(request, responseType: responseType)
    }
    
    func delete(from urlString: String) async throws {
        guard let url = URL(string: urlString) else {
            throw HTTPClientError.invalidURL
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = "DELETE"
        
        let (_, response) = try await session.data(for: request)
        
        guard let httpResponse = response as? HTTPURLResponse,
              (200...299).contains(httpResponse.statusCode) else {
            throw HTTPClientError.statusCode((response as? HTTPURLResponse)?.statusCode ?? 0)
        }
    }
}

2.2. Пример использования с Codable моделями

struct User: Codable {
    let id: Int
    let name: String
    let email: String
}

struct CreateUserRequest: Encodable {
    let name: String
    let email: String
}

// Использование
let client = HTTPClient()
do {
    // GET запрос
    let users: [User] = try await client.get(from: "https://api.example.com/users", responseType: [User].self)
    
    // POST запрос
    let newUser = CreateUserRequest(name: "Иван Иванов", email: "ivan@example.com")
    let createdUser: User = try await client.post(to: "https://api.example.com/users", body: newUser, responseType: User.self)
    
    print("Успешно: \(createdUser.name)")
} catch HTTPClientError.statusCode(let code) {
    print("Ошибка сервера: \(code)")
} catch HTTPClientError.decodingError(let error) {
    print("Ошибка декодирования: \(error)")
} catch {
    print("Неизвестная ошибка: \(error)")
}

3. Обработка ошибок и таймауты

Надёжный HTTP клиент должен корректно обрабатывать различные сценарии сбоев: отсутствие интернета, медленное соединение, неправильные данные от сервера.

3.1. Настройка таймаутов и повторные попытки

final class HTTPClientWithRetry: HTTPClient {
    private let maxRetries: Int
    private let retryDelay: TimeInterval
    
    init(maxRetries: Int = 3, retryDelay: TimeInterval = 2.0) {
        self.maxRetries = maxRetries
        self.retryDelay = retryDelay
        
        let config = URLSessionConfiguration.default
        config.timeoutIntervalForRequest = 30
        config.timeoutIntervalForResource = 60
        config.waitsForConnectivity = true
        
        super.init(session: URLSession(configuration: config))
    }
    
    override func get<T: Decodable>(from urlString: String, responseType: T.Type) async throws -> T                

Блоги

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