Введение: почему правильный 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