Una biblioteca de networking ligera, lista para Swift 6, diseñada para apps iOS modernas con async/await, arquitectura limpia y abstracciones testeables.
🌍 Idioma · English · Español · Português (Brasil) · 日本語 · 简体中文 · 한국어 · Русский
- ✅ API nativa con
async/await - ✅ Capa de networking basada en protocolos, completamente mockeable
- ✅ Decodificación tipada de request / response
- ✅ Compatible con Swift 6 + Swift Concurrency
- ✅ Diseñada para MVVM / Clean Architecture
- ✅ Sin dependencias de terceros
- ✅ Transports con respuestas predefinidas para testing
- ✅ Reintentos con backoff exponencial y jitter
- ✅ Renovación de tokens y re-autenticación transparente
- ✅ GET condicional vía ETag / If-None-Match / 304
💬 Únete a la discusión. Comentarios y preguntas son bienvenidos
Se incluye una app de demostración en SwiftUI en este repositorio usando una referencia local al paquete.
- Clona el repositorio:
git clone https://github.com/gentle-giraffe-apps/GentleNetworking.git
- Abre el proyecto de demostración:
Demo/GentleNetworkingDemo/GentleNetworkingDemo.xcodeproj
- Selecciona un simulador con iOS 17+.
- Compila y ejecuta (⌘R).
El proyecto viene preconfigurado con una referencia local al paquete Swift GentleNetworking y debería funcionar sin configuración adicional.
- Abre tu proyecto en Xcode
- Ve a File → Add Packages...
- Ingresa la URL del repositorio:
https://github.com/gentle-giraffe-apps/GentleNetworking.git - Elige una regla de versión (o
maindurante el desarrollo) - Agrega el producto GentleNetworking a tu target
Agrega la dependencia a tu Package.swift:
dependencies: [
.package(url: "https://github.com/gentle-giraffe-apps/GentleNetworking.git", from: "1.0.0")
]Luego agrega "GentleNetworking" al target que lo necesite:
.target(
name: "YourApp",
dependencies: ["GentleNetworking"]
)Este proyecto aplica controles de calidad mediante CI y análisis estático:
- CI: Todos los commits a
maindeben pasar las verificaciones de GitHub Actions - Análisis estático: DeepSource se ejecuta en cada commit a
main. El badge indica el número actual de issues de análisis estático pendientes. - Cobertura de tests: Codecov reporta la cobertura de líneas para la rama
main
Estas verificaciones están diseñadas para mantener el sistema seguro a medida que evoluciona.
GentleNetworking está centrado en un único HTTPNetworkService basado en protocolos que coordina las peticiones usando abstracciones inyectadas de endpoint, entorno y autenticación.
flowchart TB
HTTP["HTTPNetworkService<br/><br/>- request(...)"]
Endpoint["EndpointProtocol<br/><br/><br/>"]
Env["APIEnvironmentProtocol<br/><br/><br/>"]
Auth["AuthServiceProtocol<br/><br/><br/>"]
HTTP --> Endpoint
HTTP --> Env
HTTP -->|inyectado| Auth
flowchart TB
APIEndpoint["APIEndpoint enum<br/><br/>case endpoint1<br/>…<br/>endpointN"]
EndpointProtocol["EndpointProtocol<br/><br/>- path<br/>- method<br/>- query<br/>- body<br/>- requiresAuth"]
APIEndpoint -->|conforma a| EndpointProtocol
import GentleNetworking
let apiEnvironment = DefaultAPIEnvironment(
baseURL: URL(string: "https://api.company.com")
)
nonisolated enum APIEndpoint: EndpointProtocol {
case signIn(username: String, password: String)
case model(id: Int)
case models
var path: String {
switch self {
case .signIn: "/api/signIn"
case .model(let id): "/api/model/\(id)"
case .models: "/api/models"
}
}
var method: HTTPMethod {
switch self {
case .signIn: .post
case .model, .models: .get
}
}
var query: [URLQueryItem]? {
switch self {
case .signIn, .model, .models: nil
}
}
var body: [String: EndpointAnyEncodable]? {
switch self {
case .signIn(let username, let password): [
"username": EndpointAnyEncodable(username),
"password": EndpointAnyEncodable(password)
]
case .model, .models: nil
}
}
var requiresAuth: Bool {
switch self {
case .model, .models: true
case .signIn(username: _, password: _): false
}
}
}let networkService = HTTPNetworkService()SystemKeyChainAuthService es la implementación integrada de AuthServiceProtocol. Almacena un token Bearer en el keychain del sistema y lo adjunta automáticamente a las peticiones de endpoints donde requiresAuth es true.
let keyChainAuthService = SystemKeyChainAuthService()
struct AuthTokenModel: Decodable, Sendable {
let token: String
}
let authTokenModel: AuthTokenModel = try await networkService.request(
to: .signIn(username: "user", password: "pass"),
via: apiEnvironment
)
try await keyChainAuthService.saveAccessToken(
authTokenModel.token
)Usa request para decodificar un único objeto de la respuesta:
struct Model: Decodable, Sendable {
let id: Int
let property: String
}
let model: Model = try await networkService.request(
to: .model(id: 123),
via: apiEnvironment
)Usa requestModels para decodificar un array de objetos de la respuesta:
let models: [Model] = try await networkService.requestModels(
to: .models,
via: apiEnvironment
)GentleNetworking proporciona una abstracción en la capa de transporte para facilitar el mocking en tests.
Retorna una respuesta fija para cualquier petición:
let transport = CannedResponseTransport(
string: #"{"id": 1, "title": "Test"}"#,
statusCode: 200
)
let networkService = HTTPNetworkService(transport: transport)Asocia peticiones por método y patrón de ruta para escenarios de test más realistas:
let transport = CannedRoutesTransport(routes: [
CannedRoute(
pattern: RequestPattern(method: .get, path: "/api/models"),
response: CannedResponse(string: #"[{"id": 1}]"#)
),
CannedRoute(
pattern: RequestPattern(method: .post, pathRegex: "^/api/model/\\d+$"),
response: CannedResponse(string: #"{"success": true}"#)
)
])
let networkService = HTTPNetworkService(transport: transport)GentleNetworking se apoya en App Transport Security (ATS) de Apple para la protección de la capa de transporte — TLS 1.2+, validación de certificados, forward secrecy — todo aplicado por el sistema operativo y habilitado por defecto.
Para apps con requisitos de seguridad elevados, usa el PinningTransport integrado con fijación de clave pública o de certificado:
import CryptoKit
// Fijación de clave pública (recomendado — sobrevive a renovaciones de certificado)
let service = HTTPNetworkService(
transport: PinningTransport(
pinnedDomains: [
"api.example.com": PublicKeyPinningEvaluator(
pinnedKeyHashes: [primaryKeyHash, backupKeyHash]
)
]
)
)
// Fijación de certificado (más simple, falla con cada renovación)
let service = HTTPNetworkService(
transport: PinningTransport(
pinnedDomains: [
"api.example.com": CertificatePinningEvaluator(
pinnedCertificates: [certDERData]
)
]
)
)Los dominios sin fijación utilizan la validación estándar de ATS. Implementa ServerTrustEvaluator para lógica de confianza personalizada.
Consulta SECURITY.md para la guía completa incluyendo mejores prácticas, evaluadores personalizados y enfoques alternativos.
GentleNetworking proporciona wrappers de transporte componibles para la lógica de reintentos y la renovación automática de tokens. Al ser transports, se apilan entre sí y con PinningTransport.
Reintenta peticiones fallidas con backoff exponencial y jitter configurable. Por defecto reintenta en 429, 500, 503 y errores de red — nunca en 401 u otros errores de cliente.
let service = HTTPNetworkService(
transport: RetryTransport(
inner: URLSessionTransport(session: .shared),
policy: RetryPolicy(
maxRetries: 3,
baseDelay: 0.5,
maxDelay: 30.0,
jitter: .full // .full | .equal | .decorrelated
)
)
)Intercepta respuestas HTTP 401, renueva el token mediante un closure proporcionado, re-autoriza la petición original y la reintenta una vez. Los 401 concurrentes se serializan para que solo ocurra una renovación.
let service = HTTPNetworkService(
transport: ReauthTransport(
inner: RetryTransport(),
authService: keyChainAuthService,
refreshToken: {
// 1. Call your refresh endpoint to obtain a new access credential
// 2. Save the new credential via authService so future requests use it
}
)
)Evita volver a descargar recursos costosos que no han cambiado. En el primer GET, el encabezado ETag del servidor y el cuerpo de la respuesta se almacenan en caché. Los GETs posteriores envían If-None-Match; si el servidor responde 304 Not Modified, se devuelve el cuerpo en caché sin transferir la carga útil nuevamente.
let service = HTTPNetworkService(
transport: ETagTransport(
inner: URLSessionTransport(session: .shared)
)
)Inyecta un ETagStoreProtocol personalizado para persistencia respaldada por disco o base de datos:
let service = HTTPNetworkService(
transport: ETagTransport(
inner: URLSessionTransport(session: .shared),
store: MyDiskETagStore()
)
)Coloca ReauthTransport en el exterior, RetryTransport en el medio, y ETagTransport en el interior:
ReauthTransport ← captura 401 después de agotar los reintentos
└─ RetryTransport ← reintenta 429/500/503 con backoff + jitter
└─ ETagTransport ← GET condicional vía ETag / 304
└─ URLSessionTransport (o PinningTransport)RetryTransport ya omite el 401 (defaultShouldRetry devuelve false), por lo que pasa los fallos de autenticación directamente a ReauthTransport sin desperdiciar reintentos. ETagTransport se ubica dentro de retry para que las peticiones reintentadas también se beneficien de la caché.
GentleNetworking está construido alrededor de:
- ✅ Predictibilidad sobre magia
- ✅ Diseño basado en protocolos
- ✅ Inyección de dependencias explícita
- ✅ Concurrencia moderna de Swift
- ✅ Testeabilidad por defecto
- ✅ Superficie de API pequeña con garantías sólidas
Es intencionalmente mínimo y evita sobre-abstraer u ocultar el comportamiento de networking.
Partes de la redacción y el refinamiento editorial en este repositorio fueron acelerados usando modelos de lenguaje grandes (incluyendo ChatGPT, Claude y Gemini) bajo diseño humano directo, validación y aprobación final. Todas las decisiones técnicas, código y conclusiones arquitectónicas son autoría y están verificadas por el mantenedor del repositorio.
Licencia MIT Libre para uso personal y comercial.
Creado por Jonathan Ritchey Gentle Giraffe Apps Senior iOS Engineer --- Swift | SwiftUI | Concurrency