Uma biblioteca de networking leve, pronta para Swift 6, projetada para apps iOS modernos usando async/await, arquitetura limpa e abstrações testáveis.
🌍 Idioma · English · Español · Português (Brasil) · 日本語 · 简体中文 · 한국어 · Русский
- ✅ API nativa com
async/await - ✅ Camada de networking baseada em protocolos, totalmente mockável
- ✅ Decodificação tipada de request / response
- ✅ Compatível com Swift 6 + Swift Concurrency
- ✅ Projetada para MVVM / Clean Architecture
- ✅ Zero dependências de terceiros
- ✅ Transports com respostas predefinidas para testes
- ✅ Retry com backoff exponencial e jitter
- ✅ Renovação de tokens e re-autenticação transparente
- ✅ GET condicional via ETag / If-None-Match / 304
💬 Participe da discussão. Feedback e perguntas são bem-vindos
Uma app de demonstração em SwiftUI executável está incluída neste repositório usando uma referência local ao pacote.
- Clone o repositório:
git clone https://github.com/gentle-giraffe-apps/GentleNetworking.git
- Abra o projeto de demonstração:
Demo/GentleNetworkingDemo/GentleNetworkingDemo.xcodeproj - Selecione um simulador com iOS 17+.
- Compile e execute (⌘R).
O projeto vem pré-configurado com uma referência local ao pacote Swift GentleNetworking e deve funcionar sem configuração adicional.
- Abra seu projeto no Xcode
- Vá em File → Add Packages...
- Insira a URL do repositório:
https://github.com/gentle-giraffe-apps/GentleNetworking.git - Escolha uma regra de versão (ou
maindurante o desenvolvimento) - Adicione o produto GentleNetworking ao seu target
Adicione a dependência ao seu Package.swift:
dependencies: [
.package(url: "https://github.com/gentle-giraffe-apps/GentleNetworking.git", from: "1.0.0")
]Em seguida, adicione "GentleNetworking" ao target que precisar:
.target(
name: "YourApp",
dependencies: ["GentleNetworking"]
)Este projeto aplica controles de qualidade via CI e análise estática:
- CI: Todos os commits em
maindevem passar nas verificações do GitHub Actions - Análise estática: DeepSource é executado em cada commit em
main. O badge indica o número atual de issues de análise estática pendentes. - Cobertura de testes: Codecov reporta a cobertura de linhas para a branch
main
Essas verificações são projetadas para manter o sistema seguro à medida que evolui.
GentleNetworking é centrado em um único HTTPNetworkService baseado em protocolos que coordena as requisições usando abstrações injetadas de endpoint, ambiente e autenticação.
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 -->|injetado| 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 é a implementação integrada do AuthServiceProtocol. Ele armazena um token Bearer no keychain do sistema e o anexa automaticamente às requisições de endpoints onde requiresAuth é 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
)Use request para decodificar um único objeto da resposta:
struct Model: Decodable, Sendable {
let id: Int
let property: String
}
let model: Model = try await networkService.request(
to: .model(id: 123),
via: apiEnvironment
)Use requestModels para decodificar um array de objetos da resposta:
let models: [Model] = try await networkService.requestModels(
to: .models,
via: apiEnvironment
)GentleNetworking fornece uma abstração na camada de transporte para facilitar o mocking em testes.
Retorna uma resposta fixa para qualquer requisição:
let transport = CannedResponseTransport(
string: #"{"id": 1, "title": "Test"}"#,
statusCode: 200
)
let networkService = HTTPNetworkService(transport: transport)Associa requisições por método e padrão de rota para cenários de teste mais 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 utiliza o App Transport Security (ATS) da Apple para proteção da camada de transporte — TLS 1.2+, validação de certificados, forward secrecy — tudo aplicado pelo sistema operacional e habilitado por padrão.
Para apps com requisitos de segurança elevados, use o PinningTransport integrado com pinning de chave pública ou de certificado:
import CryptoKit
// Pinning de chave pública (recomendado — sobrevive a renovações de certificado)
let service = HTTPNetworkService(
transport: PinningTransport(
pinnedDomains: [
"api.example.com": PublicKeyPinningEvaluator(
pinnedKeyHashes: [primaryKeyHash, backupKeyHash]
)
]
)
)
// Pinning de certificado (mais simples, falha a cada renovação)
let service = HTTPNetworkService(
transport: PinningTransport(
pinnedDomains: [
"api.example.com": CertificatePinningEvaluator(
pinnedCertificates: [certDERData]
)
]
)
)Domínios sem pinning utilizam a validação padrão do ATS. Implemente ServerTrustEvaluator para lógica de confiança personalizada.
Consulte SECURITY.md para o guia completo incluindo melhores práticas, avaliadores personalizados e abordagens alternativas.
GentleNetworking fornece wrappers de transporte combináveis para lógica de retry e renovação automática de tokens. Por serem transports, eles se empilham entre si e com PinningTransport.
Repete requisições que falharam com backoff exponencial e jitter configurável. Por padrão, repete em 429, 500, 503 e erros de rede — nunca em 401 ou outros erros 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 respostas HTTP 401, renova o token via closure fornecido, re-autoriza a requisição original e a repete uma vez. 401s concorrentes são serializados para que apenas uma renovação ocorra.
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 o re-download de recursos caros e inalterados. No primeiro GET, o cabeçalho ETag do servidor e o corpo da resposta são armazenados em cache. GETs subsequentes enviam If-None-Match; se o servidor responder 304 Not Modified, o corpo em cache é retornado sem transferir o payload novamente.
let service = HTTPNetworkService(
transport: ETagTransport(
inner: URLSessionTransport(session: .shared)
)
)Injete um ETagStoreProtocol personalizado para persistência em disco ou banco de dados:
let service = HTTPNetworkService(
transport: ETagTransport(
inner: URLSessionTransport(session: .shared),
store: MyDiskETagStore()
)
)Coloque ReauthTransport no exterior, RetryTransport no meio, e ETagTransport no interior:
ReauthTransport ← captura 401 após esgotar os retries
└─ RetryTransport ← repete 429/500/503 com backoff + jitter
└─ ETagTransport ← GET condicional via ETag / 304
└─ URLSessionTransport (ou PinningTransport)
RetryTransport já ignora 401 (defaultShouldRetry retorna false), então passa falhas de autenticação diretamente para ReauthTransport sem desperdiçar retries. ETagTransport fica dentro do retry para que requisições refeitas também se beneficiem do cache.
GentleNetworking é construído em torno de:
- ✅ Previsibilidade acima de mágica
- ✅ Design baseado em protocolos
- ✅ Injeção de dependências explícita
- ✅ Concorrência moderna do Swift
- ✅ Testabilidade por padrão
- ✅ Superfície de API pequena com garantias sólidas
É intencionalmente mínimo e evita sobre-abstrair ou ocultar o comportamento de networking.
Partes da redação e do refinamento editorial neste repositório foram acelerados usando modelos de linguagem grandes (incluindo ChatGPT, Claude e Gemini) sob design humano direto, validação e aprovação final. Todas as decisões técnicas, código e conclusões arquiteturais são de autoria e verificação do mantenedor do repositório.
Licença MIT Livre para uso pessoal e comercial.
Criado por Jonathan Ritchey Gentle Giraffe Apps Senior iOS Engineer --- Swift | SwiftUI | Concurrency