Introducción: de GET básicos a un cliente HTTP industrial
Una solicitud HTTP estándar en Swift son tres líneas de código con URLSession.shared.dataTask. Pero en proyectos reales (descarga de archivos, autenticación con tokens, almacenamiento en caché, reintentos) esto no es suficiente. En este artículo analizaremos técnicas avanzadas para crear un cliente HTTP en Swift para iOS y macOS: protocolos personalizados, gestión de sesiones, multitarea con async/await y Combine, así como un manejo robusto de errores. El material está dirigido a desarrolladores que ya conocen los fundamentos de URLSession.
Arquitectura de un cliente HTTP avanzado
Un cliente industrial no es solo una función de solicitud, sino una capa de abstracción. Lo dividiremos en tres niveles:
- Transport Layer — trabajo de bajo nivel con URLSession, gestión de configuración.
- Service Layer — serialización/deserialización, manejo de códigos de estado, reintentos.
- Application Layer — lógica de negocio, almacenamiento en caché, autenticación.
Comenzaremos con la personalización del Transport Layer mediante URLProtocol.
1. URLProtocol personalizado para interceptar y modificar solicitudes
URLProtocol es un mecanismo potente para interceptar todas las solicitudes de red en una aplicación. Con él se puede agregar registro de logs, firma de solicitudes o datos simulados para pruebas.
import Foundation
class LoggingProtocol: URLProtocol { override class func canInit(with request: URLRequest) -> Bool { // Interceptamos todas las solicitudes HTTP/HTTPS return request.url?.scheme == "http" || request.url?.scheme == "https" }
override class func canonicalRequest(for request: URLRequest) -> URLRequest { return request }
override func startLoading() { print("📤 Solicitud: \\(request.httpMethod ?? "GET") \\(request.url?.absoluteString ?? "")") print("📦 Encabezados: \\(request.allHTTPHeaderFields ?? [:])")
let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in guard let self = self else { return } if let error = error { print("❌ Error: \\(error.localizedDescription)") self.client?.urlProtocol(self, didFailWithError: error) } else if let data = data, let response = response { print("✅ Respuesta: \\(response.expectedContentLength) bytes") self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed) self.client?.urlProtocol(self, didLoad: data) } self.client?.urlProtocolDidFinishLoading(self) } task.resume() }
override func stopLoading() { // No hacemos nada, la tarea finaliza por sí sola }}
// Registro del protocoloURLProtocol.registerClass(LoggingProtocol.self)let config = URLSessionConfiguration.defaultconfig.protocolClasses = [LoggingProtocol.self]let session = URLSession(configuration: config)Ahora todas las solicitudes a través de esta sesión quedarán registradas. De manera similar, se puede agregar la adición automática de un token Bearer o el manejo del estado 401.
2. Gestión de sesiones: configuración personalizada y multitarea
Utilice diferentes URLSessionConfiguration para distintos propósitos: .default para solicitudes normales, .ephemeral para datos privados (sin caché ni cookies), .background para descargas en segundo plano.
let backgroundConfig = URLSessionConfiguration.background(withIdentifier: "com.pythonlib.background")backgroundConfig.isDiscretionary = truebackgroundConfig.sessionSendsLaunchEvents = truelet backgroundSession = URLSession(configuration: backgroundConfig, delegate: self, delegateQueue: nil)Para multitarea, use async/await (Swift 5.5+):
func performRequest(url: URL) async throws -> Data { let (data, response)