Introduction : pourquoi un client HTTP correct est important pour votre application Swift
Les applications iOS et macOS modernes ne peuvent rarement se passer d'interaction avec des serveurs distants. Qu'il s'agisse de charger des données depuis une API REST, d'envoyer des analytics ou de travailler avec des services cloud — le client HTTP en Swift est le fondement de la communication réseau. Cependant, de nombreux développeurs, surtout les débutants, commettent des erreurs typiques : ils ignorent la gestion des erreurs, n'utilisent pas les fonctionnalités modernes du langage (async/await) ou créent du « code spaghetti » à partir d'appels réseau. Dans cet article, nous allons voir comment écrire un client HTTP fiable et performant en Swift, en suivant les meilleures pratiques.
1. Les bases de URLSession : le cœur des requêtes réseau
Dans la bibliothèque standard de Swift, le framework Foundation et la classe URLSession sont responsables de la communication réseau. C'est un outil puissant qui prend en charge la configuration des sessions, la mise en cache, les requêtes en arrière-plan et bien plus encore.
1.1. Création d'un client HTTP de base
Commençons par une structure simple qui effectuera des requêtes GET. Utilisons une énumération pour décrire les erreurs possibles et un protocole pour la flexibilité :
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. Pourquoi async/await ?
Avant l'arrivée de Swift 5.5, les développeurs utilisaient des closures (completion handlers) ou des bibliothèques tierces (Alamofire). Async/await rend le code linéaire, compréhensible et facile à lire. Les erreurs sont gérées via do-catch, et non via des closures imbriquées.
2. POST, PUT, DELETE : travailler avec différentes méthodes HTTP
Un vrai client HTTP doit prendre en charge toutes les méthodes principales. Ajoutons au protocole des méthodes pour envoyer des données et mettre à jour des ressources.
2.1. Extension du protocole pour POST et 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 a
```