A production-ready, resilient networking toolkit for iOS built on top of URLSession.
| Swift Version | Platforms | License | Swift Package Manager | CocoaPods |
|---|---|---|---|---|
| 6.0 | iOS 15+ | MIT | β Supported | β Supported |
- Protocol-oriented design with Dependency Inversion: the app depends only on
EndpointandResilientNetworkKit, not onURLSession. - Composable Decorator implementations (
CircuitBreakerNetworkKit,AuthTokenNetworkKit,AdvancedRetryNetworkKit) around a single networking abstraction. - Chain-of-Responsibility interceptors for logging, metrics, SSL pinning and request/response customization.
- Hexagonal / ports-and-adapters split into interfaces, implementation and mocks for clean testing and modularity.
ResilientNetworkKit is designed for real-world mobile apps where failures are normal β flaky networks, expired tokens, back-end incidents, and SSL requirements. It gives you:
- A clean
Endpointabstraction so you can build a NetworkKit quickly and keep request logic consistent. - Full control & observability of all network traffic via metrics, logging, SSL pinning and interceptors.
- Advanced resilience patterns out of the box: advanced retry, token refresh, and circuit breaker.
Pain points
- Every project re-invents a networking layer around
URLSession. - Hard to keep request definitions consistent across teams and modules.
- Setting up retries, timeouts, parsing, and priorities often ends up as scattered code.
How ResilientNetworkKit helps
- A single, strongly-typed
Endpointprotocol for all requests. - A
ResilientNetworkKitprotocol that abstracts sending requests (async/await or completion). - A fluent
ResilientNetworkKitBuilderto assemble your NetworkKit instance in a few lines.
Pain points
- No unified way to observe performance and errors across all requests.
- SSL pinning is tricky to get right and easy to forget.
- Different teams need different logging & metrics pipes (e.g. Datadog, Firebase, internal tools).
How ResilientNetworkKit helps
- Metrics via
MetricInterceptorandURLSessionTaskMetricsProtocolfor latency, bytes sent/received, etc. - SSL pinning via
SSLConfigurationandSSLHostIdentity, implemented safely inSecurityTrustImp. - Logging via
NetworkLogTrackerand customizableResponseMonitorInterceptor. - Traceability via
NetworkTraceInspector&NetworkRequestStatusto reconstruct any request.
Pain points
- Handling refresh token flows correctly is hard and easy to race-condition.
- Blanket retry policies can overload struggling backends.
- Without a circuit breaker, your app keeps hammering a broken service.
How ResilientNetworkKit helps
- Advanced retry per error/endpoint via
AdvancedRetryInterceptorandRetryPolicy. - Centralized token refresh via
TokenRefreshingInterceptorandAuthTokenNetworkKit. - Circuit breaker via
CircuitBreakerConfiguration&CircuitBreakerNetworkKitto protect your backend and fail fast when a service is clearly unhealthy.
Define every request as a value type that conforms to Endpoint:
public protocol Endpoint: Hashable, CustomStringConvertible, Identifiable, Sendable {
associatedtype Response: Decodable & Sendable
var id: String { get }
var url: URL { get }
var method: HTTPMethod { get }
var query: [String: AnyHashable] { get set }
var headers: [String: String] { get set }
var httpBodyEncoding: HTTPBodyEncoding { get }
var priority: Float { get }
var timeout: TimeInterval { get }
var cachePolicy: URLRequest.CachePolicy { get }
var responseParser: ResponseParser { get }
var isRefreshTokenEndpoint: Bool { get }
}The main abstraction your app depends on:
public protocol ResilientNetworkKit: Sendable {
@Sendable
func send<E: Endpoint>(_ endpoint: E,
retry: RetryPolicy,
receiveOn queue: DispatchQueueType) async throws
-> (E.Response, Int, ResilientNetworkKitHeaders)
@Sendable
func send<E: Endpoint>(_ endpoint: E,
retry: RetryPolicy,
receiveOn queue: DispatchQueueType,
completion: @Sendable @escaping (Result<(E.Response, Int, ResilientNetworkKitHeaders), ResilientNetworkKitError>) -> Void)
@Sendable
func cancelAllRequest(completingOn queue: DispatchQueueType, completion: (@Sendable () -> Void)?)
}This is what you inject into view models, controllers and use cases. The implementation (ResilientNetworkKitImp
and decorators like CircuitBreakerNetworkKit, AuthTokenNetworkKit, AdvancedRetryNetworkKit) are hidden
behind the protocol.
A fluent builder used to compose the full NetworkKit stack:
- Plug in logging (
NetworkLogTracker) - Configure SSL & metrics (
SessionDelegateConfiguration&SSLConfiguration) - Add request interceptors (
RequestInterceptor) - Add advanced behaviors (
AdvancedRetryInterceptor,TokenRefreshingInterceptor,CircuitBreakerConfiguration)
ResilientNetworkKit is structured to make clean code, testability and observability the default.
-
Protocol-oriented & dependency inversion
Your app depends only on theEndpointandResilientNetworkKitprotocols, not on concreteURLSessionAPIs. This follows the Dependency Inversion Principle (DIP) and keeps networking behind a single, stable abstraction. -
Decorator pattern for cross-cutting concerns
Types likeCircuitBreakerNetworkKit,AuthTokenNetworkKitandAdvancedRetryNetworkKitall conform toResilientNetworkKitand wrap anotherResilientNetworkKitinstance. This is the classic Decorator pattern: you can layer behaviors (circuit breaker, retries, token refresh) without changing call sites. -
Builder pattern for configuration
ResilientNetworkKitBuilderimplements the Builder pattern: it assembles the base implementation, decorators, interceptors, SSL and metrics into a single pipeline. There is exactly one place where you configure how networking behaves for the entire app. -
Chain of Responsibility via interceptors
Request/response interceptors form a Chain of Responsibility: each interceptor can observe, modify or short-circuit requests and responses in order, without knowing about the others. This makes logging, metrics, tracing and header manipulation pluggable and easy to test. -
Value-based endpoints and strong typing
Endpointtypes areHashable,IdentifiableandSendable, and carry a strongly typedResponse. This makes them safe to cache, log and pass across threads, and gives you end-to-end type safety from request definition to decoded response. -
Interfaces / implementation / mocks split
The project is physically split intointerfaces,implementationandmockstargets. This mirrors ports & adapters / hexagonal architecture: your app depends only on interfaces while concrete implementations and test doubles live in separate modules. -
Full control over networking
All network traffic flows through this composable pipeline, so you have central control over logging, metrics, SSL pinning, retries, token refresh and circuit breaking. You can enforce organization-wide policies and diagnose issues from a single, well-defined layer.
Xcode
- Go to File β Add Packagesβ¦
- Enter the URL of this repository:
https://github.com/harryngict/ResilientNetworkKit.git - Add the ResilientNetworkKit (and optionally ResilientNetworkKitImp, ResilientNetworkKitMock) products to your target.
Package.swift
.dependencies: [
.package(url: "https://github.com/harryngict/ResilientNetworkKit.git", from: "0.1.0"),
],
.targets: [
.target(
name: "YourFeature",
dependencies: [
.product(name: "ResilientNetworkKit", package: "ResilientNetworkKit"),
.product(name: "ResilientNetworkKitImp", package: "ResilientNetworkKit"),
]
),
]# Podfile
source 'https://cdn.cocoapods.org/'
platform :ios, '15.0'
use_frameworks!
pod 'ResilientNetworkKit', :git => 'https://github.com/harryngict/ResilientNetworkKit.git'
pod 'ResilientNetworkKitImp', :git => 'https://github.com/harryngict/ResilientNetworkKit.git'
# Optional β for tests
pod 'ResilientNetworkKitMock', :git => 'https://github.com/harryngict/ResilientNetworkKit.git'Then run:
pod installimport ResilientNetworkKit
struct UsersEndpoint: Endpoint, @unchecked Sendable {
typealias Response = [User]
var query: [String : AnyHashable] = [:]
var headers: [String : String] = [:]
var url: URL { URL(string: "https://jsonplaceholder.typicode.com/users")! }
var method: HTTPMethod { .get }
}import ResilientNetworkKit
import ResilientNetworkKitImp
final class AppNetworkProvider {
let networkKit: ResilientNetworkKit
init() {
let logger = NetworkLogTrackerImp()
let builder = ResilientNetworkKitBuilder()
networkKit = builder
.withNetworkLogTracker(logger)
.withSessionDelegate(.default) // SSL off by default, metrics off by default
.withResponseMonitorInterceptor(ClientResponseMonitorInterceptorImp(networkLogTracker: logger))
.withNetworkTraceInspector(nil) // or your implementation
.withTokenRefreshingInterceptor(self) // implements TokenRefreshingInterceptor
.withCircuitBreakerConfig(CircuitBreakerConfiguration())
.withAdvancedRetryInterceptor(AdvancedRetryInterceptorImp())
.addRequestInterceptors([AuthHeaderInterceptor()])
.build()
}
}Where AuthHeaderInterceptor is a simple RequestInterceptor:
import ResilientNetworkKit
final class AuthHeaderInterceptor: RequestInterceptor {
func modify<E: Endpoint>(endpoint: E) -> E {
var copy = endpoint
copy.headers["Authorization"] = "Bearer <token>"
return copy
}
}let endpoint = UsersEndpoint()
Task {
do {
let (users, statusCode, headers) = try await networkKit.send(
endpoint,
retry: .constant(count: 2, delay: 1.0)
)
print("Loaded", users.count, "users", "status:", statusCode)
} catch {
print("Networking failed", error)
}
}networkKit.send(UsersEndpoint(), retry: .none) { result in
switch result {
case let .success((users, statusCode, _)):
print("Loaded", users.count, "users", "status:", statusCode)
case let .failure(error):
print("Networking failed", error)
}
}Implement AdvancedRetryInterceptor to customize retry based on error or endpoint:
import ResilientNetworkKit
final class MyAdvancedRetryInterceptor: AdvancedRetryInterceptor {
func getRetryPolicy(_ endPoint: some Endpoint, error: ResilientNetworkKitError) -> RetryPolicy? {
// Retry 5 times with 5s delay on 503/504 only
guard [503, 504].contains(error.statusCode) else { return nil }
return .constant(count: 5, delay: 5.0)
}
}Then plug it into the builder:
.withAdvancedRetryInterceptor(MyAdvancedRetryInterceptor())Centralize your refresh token logic with TokenRefreshingInterceptor:
import ResilientNetworkKit
final class TokenRefresher: TokenRefreshingInterceptor {
private let networkKit: ResilientNetworkKit
init(networkKit: ResilientNetworkKit) {
self.networkKit = networkKit
}
func refreshAccessToken(completion: @escaping (Result<Void, ResilientNetworkKitError>) -> Void) {
let refreshEndpoint = RefreshTokenEndpoint()
networkKit.send(refreshEndpoint) { result in
switch result {
case .success:
// Update your token storage here
completion(.success(()))
case let .failure(error):
completion(.failure(error))
}
}
}
}Attach it:
.withTokenRefreshingInterceptor(TokenRefresher(networkKit: networkKit))ResilientNetworkKit will:
- Queue requests while a refresh is in progress.
- Retry queued requests once refresh succeeds.
Protect your backend from continuous failures using CircuitBreakerConfiguration:
let circuitConfig = CircuitBreakerConfiguration(
failureThreshold: 8,
openTimeout: 15,
halfOpenMaxRequests: 3
)
let networkKit = ResilientNetworkKitBuilder()
.withCircuitBreakerConfig(circuitConfig)
.build()The CircuitBreakerNetworkKit decorator will:
- Open the circuit after repeated failures and immediately fail new calls with
.circuitBreakerOpen. - Transition back to half-open/closed when conditions improve.
- Implement
MetricInterceptorto forwardURLSessionTaskMetricsto your observability stack. - Provide
SSLConfiguration(isPinningEnabled:pinnedHostIdentities:)to enable SSL pinning. - Implement
NetworkTraceInspectorto store a timeline of requests (for in-app debugging UIs). - Implement
NetworkLogTrackerto centralize all networking logs.
- Your application code talks only to the
ResilientNetworkKitprotocol β never toURLSessiondirectly. This is a classic ports & adapters setup and keeps networking behind a single abstraction. - In production, you inject the concrete pipeline built by
ResilientNetworkKitBuilder. In tests, you inject a fake or a type from theResilientNetworkKitMockmodule (available as a separate SPM / CocoaPods product). - All cross-cutting components (
MetricInterceptor,NetworkLogTracker,NetworkTraceInspector, SSL and retry/token-refresh interceptors) are protocols, so they can be replaced with lightweight fakes in unit tests. - Because interceptors and decorators are composable, you can write focused tests for:
- "no network" flows (inject a mock that returns canned responses),
- resilience behavior (inject a decorator and assert on retries / circuit-breaker state),
- observability (inject test loggers / metrics collectors).
- Interfaces (
Sources/ResilientNetworkKit/interfaces): public protocols & value types your app depends on. - Implementation (
Sources/ResilientNetworkKit/implementation): concrete implementations, decorators and builders (ResilientNetworkKitImp, circuit breaker, token refresh, retry, SSL, metrics, etc.). - Mocks (
Sources/ResilientNetworkKit/mocks): mocks tailored for unit testing.
This separation makes the library suitable for large, modular iOS codebases, and friendly to dependency injection and testing.
graph LR
App[Your iOS App] -->|Endpoint requests| RNK[ResilientNetworkKit]
RNK --> Builder[ResilientNetworkKitBuilder]
Builder --> Decorators[Decorators]
Decorators --> Session[URLSession + SessionDelegate]
Session --> Interceptors[Interceptors]
Decorators typically include:
AdvancedRetryNetworkKitAuthTokenNetworkKitCircuitBreakerNetworkKit
Interceptors typically include:
- Metrics (via
MetricInterceptor) - Logging (via
NetworkLogTracker/ response monitor interceptors) - SSL pinning (via
SSLConfiguration) - Token refresh (via
TokenRefreshingInterceptor)
Contributions are welcome! Please feel free to submit a Pull Request.
If you find this library useful, please consider giving it a star! It helps others discover the project.
This project is licensed under the MIT License - see the LICENSE file for details.
Harry Nguyen Chi Hoang
- Email: [email protected]
- GitHub: @harryngict