Durante casi una década, Alamofire ha sido el rey indiscutible de las dependencias en el desarrollo iOS. Facilitaba la gestión de peticiones HTTP, el parseo de JSON y el manejo de hilos cuando URLSession era verboso y propenso a errores. Sin embargo, el panorama de Swift ha cambiado radicalmente.
Con la llegada de Swift 6 y la Concurrencia Estricta (Strict Concurrency), depender de librerías externas de terceros para tareas fundamentales como la comunicación con una API REST ya no solo es innecesario, sino que puede introducir advertencias y errores de compilación difíciles de resolver.
En este artículo, aprenderás a diseñar e implementar una capa de red moderna, robusta, genérica y 100% nativa utilizando Swift 6, async/await y adaptada a las exigencias de seguridad de hilos (thread-safety) que exige el nuevo compilador.
¿Por qué decirle adiós a Alamofire en la era de Swift 6?
Mantener dependencias externas tiene un costo: aumenta el tiempo de compilación, el tamaño del binario y el riesgo de problemas de seguridad. Pero la razón principal para abandonar Alamofire hoy es tecnológica:
- URLSession maduro: Desde iOS 15,
URLSessioncuenta con soporte nativo paraasync/await, eliminando el infierno de los closures de escape (callback hell). - Concurrencia Estricta en Swift 6: Swift 6 de forma predeterminada previene las carreras de datos (data races) en tiempo de compilación. Las librerías que no estén perfectamente adaptadas a
@Sendableyactorsgenerarán advertencias masivas. - Menos boilerplate: Crear un cliente de red genérico hoy requiere menos de 100 líneas de código limpio y mantenible.
El Desafío de Swift 6: Concurrencia Estricta
Bajo el modelo de concurrencia de Swift 6, el compilador garantiza que los datos no se compartan entre hilos de forma insegura. Para lograr esto, introduce dos conceptos clave:
- Protocolo
Sendable: Indica que un tipo se puede transferir de forma segura entre entornos concurrentes. - Actors: Tipos de referencia que protegen su estado interno del acceso concurrente.
Nuestra capa de red debe garantizar que tanto las peticiones (Request) como las respuestas (Response) y los manejadores de errores sean Sendable.
Diseñando la Capa de Red Moderna
Estructuraremos nuestra arquitectura en tres niveles de abstracción:
- Endpoint: Un protocolo que define los requisitos de cualquier petición HTTP.
- NetworkError: Un enum seguro para el manejo de fallos.
- APIClient: Un actor que ejecutará las peticiones de forma asíncrona y segura.
1. Definición del Endpoint y Métodos HTTP
En lugar de construir URLs dinámicas directamente en el código, utilizaremos un enfoque tipado y declarativo.
import Foundation
// Definición de los métodos HTTP compatibles
enum HTTPMethod: String, Sendable {
case get = "GET"
case post = "POST"
case put = "PUT"
case delete = "DELETE"
}
// Protocolo para estructurar cualquier petición a la API
protocol Endpoint: Sendable {
var baseURL: String { get }
var path: String { get }
var method: HTTPMethod { get }
var headers: [String: String]? { get }
var queryParameters: [String: String]? { get }
var body: (any Encodable & Sendable)? { get }
}
extension Endpoint {
// Implementación por defecto para construir la URLRequest
func asURLRequest() throws -> URLRequest {
guard var components = URLComponents(string: baseURL + path) else {
throw NetworkError.invalidURL
}
// Agregar Query Parameters si existen
if let queryParameters {
components.queryItems = queryParameters.map { URLQueryItem(name: $0.key, value: $0.value) }
}
guard let url = components.url else {
throw NetworkError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = method.rawValue
// Agregar Headers
headers?.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
// Agregar Body si existe
if let body {
let encoder = JSONEncoder()
request.httpBody = try encoder.encode(body)
}
return request
}
}
2. Manejo de Errores con Tipado Seguro
Definimos un enum que implementa Error y cumple con Sendable para cumplir con las directrices de Swift 6.
enum NetworkError: Error, LocalizedError, Sendable {
case invalidURL
case requestFailed(statusCode: Int)
case decodingFailed(Error)
case unknown(Error)
var errorDescription: String? {
switch self {
case .invalidURL:
return "La URL proporcionada no es válida."
case .requestFailed(let statusCode):
return "La petición falló con el código de estado: \(statusCode)."
case .decodingFailed(let error):
return "Error al procesar los datos de respuesta: \(error.localizedDescription)"
case .unknown(let error):
return "Ocurrió un error inesperado: \(error.localizedDescription)"
}
}
}
3. El Cliente de Red: Implementando el Actor APIClient
Para garantizar la seguridad de hilos sin usar semáforos o colas de despacho manuales (DispatchQueue), definimos nuestro cliente de red como un actor. Los actors garantizan el acceso serializado a sus métodos y propiedades.
import Foundation
protocol APIClientProtocol: Sendable {
func request(_ endpoint: any Endpoint) async throws -> T
}
actor APIClient: APIClientProtocol {
private let session: URLSession
// Inicializador que permite inyectar una configuración de URLSession para testing
init(session: URLSession = .shared) {
self.session = session
}
func request(_ endpoint: any Endpoint) async throws -> T {
let request = try endpoint.asURLRequest()
let data: Data
let response: URLResponse
do {
// URLSession asíncrono nativo de Swift
(data, response) = try await session.data(for: request)
} catch {
throw NetworkError.unknown(error)
}
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.requestFailed(statusCode: 999)
}
// Validar rango de respuesta exitosa (200-299)
guard (200...299).contains(httpResponse.statusCode) else {
throw NetworkError.requestFailed(statusCode: httpResponse.statusCode)
}
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase // Opcional, según tu API
return try decoder.decode(T.self, from: data)
} catch {
throw NetworkError.decodingFailed(error)
}
}
}
Implementación Práctica: Consumiendo la API
Veamos cómo usar esta capa de red para descargar información de usuarios de una API de prueba (reqres.in).
1. Definir los Modelos de Datos
Los modelos deben ser Decodable y Sendable.
struct User: Decodable, Sendable {
let id: Int
let email: String
let firstName: String
let lastName: String
let avatar: String
}
struct UserResponse: Decodable, Sendable {
let data: [User]
}
2. Definir el Endpoint específico
enum UserEndpoint: Endpoint {
case getUsers(page: Int)
var baseURL: String {
return "https://reqres.in/api"
}
var path: String {
switch self {
case .getUsers:
return "/users"
}
}
var method: HTTPMethod {
return .get
}
var headers: [String : String]? {
return nil
}
var queryParameters: [String : String]? {
switch self {
case .getUsers(let page):
return ["page": String(page)]
}
}
var body: (any Encodable & Sendable)? {
return nil
}
}
3. Consumo en el ViewModel con @MainActor
Los ViewModels que interactúan con la UI deben actualizarse en el hilo principal (@MainActor). Gracias a async/await, la transición entre el hilo de red (gestionado por el actor APIClient) y el hilo principal es transparente y segura.
import SwiftUI
@MainActor
class UserViewModel: ObservableObject {
@Published var users: [User] = []
@Published var errorMessage: String?
@Published var isLoading = false
// Inyección de dependencias para facilitar testing
private let apiClient: any APIClientProtocol
init(apiClient: any APIClientProtocol = APIClient()) {
self.apiClient = apiClient
}
func fetchUsers() async {
isLoading = true
errorMessage = nil
do {
let response: UserResponse = try await apiClient.request(UserEndpoint.getUsers(page: 1))
self.users = response.data
} catch let error as NetworkError {
self.errorMessage = error.errorDescription
} catch {
self.errorMessage = error.localizedDescription
}
isLoading = false
}
}
Ventajas Clave del Enfoque Moderno de Swift 6
Al migrar de Alamofire a este diseño nativo, obtienes beneficios inmediatos:
- Seguridad contra Data Races: Al utilizar un
actorpara el cliente de red y tipos conformes aSendable, el compilador de Swift 6 garantiza que no habrá colisiones de memoria en accesos concurrentes. - Cero dependencias externas: Reduce el tamaño de tu App Store Package y evita los retrasos de actualización cuando Apple lanza nuevas versiones de Xcode y Swift.
- Testabilidad óptima: El uso de protocolos como
APIClientProtocolfacilita el mockeo del cliente de red para pruebas unitarias rápidas y deterministicas, sin tocar la red real. - Legibilidad: El código se lee de forma secuencial de arriba a abajo, evitando la anidación excesiva de llaves (
{}).
Conclusión
El ecosistema de Apple ha evolucionado de manera asombrosa. Alamofire fue una herramienta vital que marcó el camino a seguir, pero en la era de Swift 6, async/await y la Concurrencia Estricta, la plataforma nativa nos ofrece todo lo necesario para construir arquitecturas de software limpias, ultra rápidas y seguras.
Dar el paso hacia una capa de red nativa no solo modernizará tu código, sino que te preparará para los rigurosos estándares de rendimiento y seguridad que Apple exige hoy en día. ¿Listo para eliminar esa línea del archivo Podfile o de tu Swift Package Manager?