URLSession es el framework de red de Apple, y desde Swift 5.5 tiene soporte completo para async/await. Las APIs basadas en callbacks y completionHandler siguen disponibles, pero los nuevos métodos async simplifican enormemente el código de networking: sin anidamiento de closures, sin riesgo de olvidar llamar al completion handler, y con cancelación automática integrada en la concurrencia estructurada.
data(from:): petición básica
El método más utilizado. Devuelve los datos y la respuesta HTTP de forma asíncrona:
struct APIClient {
let sesion = URLSession.shared
func fetchUsuario(id: Int) async throws -> Usuario {
let url = URL(string: "https://api.example.com/usuarios/(id)")!
let (datos, respuesta) = try await sesion.data(from: url)
guard let http = respuesta as? HTTPURLResponse else {
throw NetworkError.respuestaInvalida
}
guard 200...299 ~= http.statusCode else {
throw NetworkError.statusCode(http.statusCode)
}
return try JSONDecoder().decode(Usuario.self, from: datos)
}
}
URLRequest: peticiones con cabeceras y método
func crearUsuario(_ usuario: NuevoUsuario, token: String) async throws -> Usuario {
var request = URLRequest(url: URL(string: "https://api.example.com/usuarios")!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer (token)", forHTTPHeaderField: "Authorization")
request.httpBody = try JSONEncoder().encode(usuario)
request.timeoutInterval = 30
let (datos, respuesta) = try await sesion.data(for: request)
try verificarRespuesta(respuesta)
return try JSONDecoder().decode(Usuario.self, from: datos)
}
func verificarRespuesta(_ respuesta: URLResponse) throws {
guard let http = respuesta as? HTTPURLResponse,
200...299 ~= http.statusCode else {
let codigo = (respuesta as? HTTPURLResponse)?.statusCode ?? -1
throw NetworkError.statusCode(codigo)
}
}
download(from:): descargar a fichero
Para descargar archivos grandes, download(from:) escribe directamente a un fichero temporal sin cargar todo en memoria:
func descargarArchivo(url: URL, destino: URL) async throws {
let (archivoTemporal, respuesta) = try await sesion.download(from: url)
try verificarRespuesta(respuesta)
// Mover desde el temporal al destino
if FileManager.default.fileExists(atPath: destino.path) {
try FileManager.default.removeItem(at: destino)
}
try FileManager.default.moveItem(at: archivoTemporal, to: destino)
}
upload(for:from:): subir datos
func subirImagen(_ imagen: UIImage, para usuarioId: Int) async throws -> URL {
guard let datos = imagen.jpegData(compressionQuality: 0.8) else {
throw UploadError.compresionFallida
}
var request = URLRequest(url: URL(string: "https://api.example.com/imagenes")!)
request.httpMethod = "POST"
request.setValue("image/jpeg", forHTTPHeaderField: "Content-Type")
let (respuestaDatos, respuesta) = try await sesion.upload(for: request, from: datos)
try verificarRespuesta(respuesta)
struct RespuestaUpload: Decodable { let url: URL }
return try JSONDecoder().decode(RespuestaUpload.self, from: respuestaDatos).url
}
bytes(from:): streaming de datos
Para procesar respuestas largas sin cargarlas enteras en memoria, bytes(from:) devuelve un AsyncSequence de bytes:
func leerLineasSSE(url: URL) async throws {
let (bytes, _) = try await URLSession.shared.bytes(from: url)
for try await linea in bytes.lines {
guard linea.hasPrefix("data: ") else { continue }
let payload = String(linea.dropFirst(6))
if let evento = try? JSONDecoder().decode(EventoSSE.self,
from: Data(payload.utf8)) {
await procesarEvento(evento)
}
}
}
bytes.lines divide el stream en líneas de texto, perfecto para Server-Sent Events o APIs de streaming como las de modelos de IA.
WebSocket con webSocketTask
Las conexiones WebSocket mantienen un canal bidireccional persistente. Con async/await, el loop de recepción es directo:
class WebSocketManager {
private var tarea: URLSessionWebSocketTask?
func conectar(url: URL) async throws {
let sesion = URLSession(configuration: .default)
tarea = sesion.webSocketTask(with: url)
tarea?.resume()
// Enviar un mensaje de autenticación
try await tarea?.send(.string("""
{"tipo": "auth", "token": "(tokenActual)"}
"""))
// Loop de recepción se ejecuta hasta que la conexión se cierra
await recibirMensajes()
}
private func recibirMensajes() async {
guard let tarea = tarea else { return }
do {
while true {
let mensaje = try await tarea.receive()
switch mensaje {
case .string(let texto):
await procesarMensaje(texto)
case .data(let datos):
await procesarDatos(datos)
@unknown default:
break
}
}
} catch {
print("WebSocket cerrado: (error)")
}
}
func enviar(_ mensaje: String) async throws {
try await tarea?.send(.string(mensaje))
}
func desconectar() {
tarea?.cancel(with: .goingAway, reason: nil)
tarea = nil
}
}
Sesiones ephemeral: sin caché ni cookies
// Para peticiones que no deben dejar rastro (modo privado, pagos, etc.)
let sesionPrivada = URLSession(configuration: .ephemeral)
let (datos, _) = try await sesionPrivada.data(from: urlSensible)
El antipatrón más frecuente: mezclar callbacks y async
// ? Antipatrón: envolver el método async en un closure de completion
func fetchDatos(completion: @escaping (Data?) -> Void) {
Task {
let (datos, _) = try? await URLSession.shared.data(from: url)
completion(datos)
}
}
// Consecuencias: pierdes la propagación de errores, la cancelación,
// y la concurrencia estructurada. Si el llamador cancela la tarea,
// la petición de red sigue ejecutándose.
// Solución: exponer directamente la función async
func fetchDatos() async throws -> Data {
let (datos, _) = try await URLSession.shared.data(from: url)
return datos
}
Cancelación correcta de peticiones
class BuscadorViewModel {
private var tareaActual: Task?
func buscar(termino: String) {
tareaActual?.cancel() // Cancela la búsqueda anterior
tareaActual = Task {
do {
let resultados = try await api.buscar(termino: termino)
// Verificar que la tarea no fue cancelada durante la petición
guard !Task.isCancelled else { return }
await MainActor.run { self.resultados = resultados }
} catch is CancellationError {
// Cancelación normal, no mostrar error
} catch {
await MainActor.run { self.error = error }
}
}
}
}
Resumen
URLSession con async/await transforma el código de networking: las peticiones simples son dos líneas, el streaming de respuestas grandes usa bytes.lines con un for await, y los WebSocket tienen un loop de recepción tan claro como un bucle normal. La cancelación se integra en la concurrencia estructurada sin código adicional. El único antipatrón a evitar es envolver APIs async en callbacks, que anula todas estas ventajas.
