Introducción: por qué un cliente HTTP correcto es importante para tu aplicación Swift
Las aplicaciones modernas de iOS y macOS rara vez prescinden de la interacción con servidores remotos. Ya sea para cargar datos desde una REST API, enviar análisis o trabajar con servicios en la nube — un cliente HTTP en Swift es la base de la comunicación en red. Sin embargo, muchos desarrolladores, especialmente los principiantes, cometen errores típicos: ignoran el manejo de errores, no utilizan las capacidades modernas del lenguaje (async/await) o crean "código espagueti" con llamadas de red. En este artículo analizaremos cómo escribir un cliente HTTP robusto y eficiente en Swift, siguiendo las mejores prácticas.
1. Fundamentos de URLSession: el corazón de las solicitudes de red
En la biblioteca estándar de Swift, la comunicación en red está a cargo del framework Foundation y la clase URLSession. Esta es una herramienta potente que soporta configuración de sesiones, almacenamiento en caché, solicitudes en segundo plano y mucho más.
1.1. Creación de un cliente HTTP básico
Comencemos con una estructura simple que realizará solicitudes GET. Usamos un enum para describir los posibles errores y un protocolo para la flexibilidad:
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. ¿Por qué async/await?
Antes de la llegada de Swift 5.5, los desarrolladores usaban closures (completion handlers) o bibliotecas de terceros (Alamofire). Async/await hace que el código sea lineal, comprensible y fácil de leer. Los errores se manejan mediante do-catch, en lugar de closures anidados.
2. POST, PUT, DELETE: trabajo con diferentes métodos HTTP
Un cliente HTTP real debe soportar todos los métodos principales. Añadamos al protocolo métodos para enviar datos y actualizar recursos.
2.1. Extensión del protocolo para POST y 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