A production-ready Swift networking library built on URLSession. Zero third-party dependencies -- 100% Apple frameworks.
- Type-safe API responses --
APIResponse<T>enum with.success,.error,.networkError,.decodingError - Automatic token refresh -- Actor-based 401 handling with concurrent request coalescing
- Retry with exponential backoff -- Configurable retry on 5xx, 408, 429; idempotency-aware
- Request deduplication -- Collapses concurrent identical GET requests into one network call
- Logging with redaction -- Full request/response logging, masks sensitive headers and JSON fields
- Certificate pinning -- SHA-256 pin validation via URLSession delegate
- Multipart uploads -- File and byte array uploads with 30+ MIME type auto-detection
- Network error classification -- 5 categories (noConnection, timeout, dnsFailure, sslError, unknown)
- Auth interceptor -- Separate
AuthInterceptorin the chain, visible to all interceptors - Per-request timeout -- Override global timeout on individual calls (uploads, health checks)
- flatMap chaining -- Chain dependent API calls with proper error propagation
- Interceptor chain -- Composable middleware pattern for request/response processing
- Swift 6 strict concurrency -- Full
Sendableconformance throughout
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/mindobix/CCSwiftNetworking.git", from: "1.0.0")
]Or add via Xcode: File > Add Package Dependencies.
let client = NetworkingClient(baseURL: "https://api.example.com") { config in
config.defaultHeaders = ["X-App-Version": "1.0", "Accept": "application/json"]
config.retryConfig = RetryConfig(maxRetries: 2)
config.enableDeduplication = true
config.loggingConfig = LoggingConfig(
level: .body,
redactHeaders: ["X-Custom-Secret"],
redactBodyFields: ["password", "ssn"]
)
config.connectTimeout = 30
config.resourceTimeout = 60
}// GET
let response: APIResponse<User> = await client.get("/users/me")
// POST with body
let response: APIResponse<User> = await client.post("/users", body: CreateUserRequest(name: "Alice", email: "alice@example.com"))
// PUT
let response: APIResponse<User> = await client.put("/users/1", body: updateRequest)
// DELETE
let response: APIResponse<EmptyResponse> = await client.delete("/users/1")
// With query parameters
let response: APIResponse<[User]> = await client.get("/users", queryItems: [
URLQueryItem(name: "page", value: "1"),
URLQueryItem(name: "limit", value: "20")
])let response: APIResponse<User> = await client.get("/users/me")
switch response {
case .success(let user, let statusCode, let headers):
print("User: \(user.name)")
case .error(let statusCode, let apiError, let rawBody):
print("HTTP \(statusCode): \(apiError?.displayMessage ?? "Unknown error")")
case .networkError(let error, let kind):
print(kind.userMessage) // "No internet connection. Please check your network settings."
case .decodingError(let error, let statusCode, let rawBody):
// Server returned 200 but response didn't match expected model
print("Decode failed (\(statusCode)): \(error)")
print("Raw body: \(rawBody ?? "nil")")
}// Health check with short timeout
let health: APIResponse<Status> = await client.get("/health", timeoutInterval: 5)
// File upload with long timeout
let upload: APIResponse<UploadResult> = await client.upload("/upload", multipart: multipart, timeoutInterval: 300)let result: APIResponse<[Order]> = await client.get("/users/me")
.flatMap { user in
await client.get("/users/\(user.id)/orders")
}await client.get("/users/me")
.onSuccess { (user: User) in
updateUI(with: user)
}
.onError { code, apiError, _ in
showError(apiError?.displayMessage ?? "Request failed")
}
.onNetworkError { _, kind in
showError(kind.userMessage)
}let nameResponse: APIResponse<String> = await client.get("/users/me")
.map { (user: User) in user.name }case .networkError(let error, let kind):
switch kind {
case .noConnection: showOfflineBanner()
case .timeout: showRetryButton()
case .dnsFailure: showServerUnreachable()
case .sslError: showSecurityWarning()
case .unknown: showGenericError()
}// 1. Implement TokenProvider
final class MyTokenProvider: TokenProvider {
func getAccessToken() async -> String? { keychain.get("accessToken") }
func getRefreshToken() async -> String? { keychain.get("refreshToken") }
func onTokensUpdated(accessToken: String, refreshToken: String) async {
keychain.set("accessToken", accessToken)
keychain.set("refreshToken", refreshToken)
}
func onAuthenticationFailed() async {
await router.navigateToLogin()
}
}
// 2. Configure the client
let client = NetworkingClient(baseURL: "https://api.example.com") { config in
config.tokenProvider = MyTokenProvider()
config.refreshTokenConfig = RefreshTokenConfig(
refreshURL: "https://api.example.com/auth/refresh",
method: .post,
accessTokenHeader: "Authorization",
refreshTokenHeader: "X-Refresh-Token"
)
config.maxAuthRetries = 1
}Request -> 401 -> TokenAuthenticator.refreshIfNeeded()
|
+-- Already refreshing? -> Wait for result
|
+-- Acquire lock -> Call refresh endpoint
|
+-- Success -> Update tokens -> Retry original request
|
+-- Failure -> onAuthenticationFailed()
let multipart = MultipartFormData()
multipart.addFile(data: imageData, name: "avatar", fileName: "photo.jpg", mimeType: MIMEType.imageJPEG)
multipart.addField(name: "caption", value: "Profile photo")
let response: APIResponse<UploadResult> = await client.upload("/upload", multipart: multipart)let multipart = MultipartFormData()
try multipart.addFile(url: fileURL, name: "document") // Auto-detects MIME type
try multipart.addFiles(urls: [file1, file2], name: "attachments")let client = NetworkingClient(baseURL: "https://api.example.com") { config in
config.certificatePins = [
"api.example.com": ["sha256/AAAA=", "sha256/BBBB="] // Primary + backup
]
}struct MetricsInterceptor: Interceptor {
func intercept(_ request: URLRequest, next: InterceptorNext) async throws -> (Data, HTTPURLResponse) {
let start = CFAbsoluteTimeGetCurrent()
let result = try await next(request)
let elapsed = CFAbsoluteTimeGetCurrent() - start
Analytics.track("api_call", duration: elapsed)
return result
}
}
let client = NetworkingClient(baseURL: "https://api.example.com") { config in
config.interceptors = [MetricsInterceptor()]
}| Option | Type | Default | Description |
|---|---|---|---|
baseURL |
String |
Required | Base URL for all requests |
tokenProvider |
TokenProvider? |
nil |
OAuth token storage |
refreshTokenConfig |
RefreshTokenConfig? |
nil |
Token refresh endpoint config |
maxAuthRetries |
Int |
1 |
Max 401 refresh attempts |
defaultHeaders |
[String: String] |
[:] |
Headers added to every request |
retryConfig |
RetryConfig? |
nil |
Retry policy (maxRetries, backoff) |
enableDeduplication |
Bool |
false |
Collapse concurrent identical GETs |
loggingConfig |
LoggingConfig? |
nil |
Logging level and redaction |
certificatePins |
[String: [String]] |
[:] |
SHA-256 pins by hostname |
connectTimeout |
TimeInterval |
30 |
Connection timeout (seconds) |
resourceTimeout |
TimeInterval |
30 |
Resource/read timeout (seconds) |
interceptors |
[Interceptor] |
[] |
Custom interceptor chain |
jsonDecoder |
JSONDecoder |
Default | Custom JSON decoder |
jsonEncoder |
JSONEncoder |
Default | Custom JSON encoder |
Sources/CCSwiftNetworking/
NetworkingClient.swift -- Main client, config, request execution
APIResponse.swift -- Type-safe response enum
APIError.swift -- Standard error body model
NetworkErrorKind.swift -- Network error classification
HTTPMethod.swift -- HTTP method enum
Interceptor.swift -- Interceptor protocol and chain
RetryInterceptor.swift -- Automatic retry with backoff
HeaderInterceptor.swift -- Default headers
LoggingInterceptor.swift -- Logging with redaction
DeduplicatingInterceptor.swift-- Request deduplication
CertificatePinning.swift -- SSL pin validation
MultipartFormData.swift -- Multipart upload builder
MIMEType.swift -- MIME type constants and detection
Auth/
AuthInterceptor.swift -- Bearer token injection interceptor
TokenProvider.swift -- Token storage protocol
RefreshTokenConfig.swift -- Refresh endpoint config
TokenAuthenticator.swift -- Actor-based 401 handling
| Test Suite | Tests | Covers |
|---|---|---|
| APIResponseTests | 33 | Success/error/networkError/decodingError, map, flatMap, callbacks |
| APIErrorTests | 15 | JSON parsing, displayMessage, field errors |
| NetworkErrorKindTests | 27 | URLError classification, NSError, message patterns |
| RetryInterceptorTests | 21 | 5xx/408/429 retry, backoff, idempotency, recovery |
| HeaderInterceptorTests | 6 | Default headers, precedence, passthrough |
| LoggingInterceptorTests | 15 | Levels, header/body redaction, timing |
| DeduplicatingInterceptorTests | 8 | Concurrent GETs, POST bypass, query params |
| InterceptorChainTests | 5 | Ordering, modification, short-circuit, errors |
| NetworkingClientBuilderTests | 12 | Configuration, defaults, custom options |
| AuthInterceptorTests | 5 | Token injection, skip existing, nil token |
| TokenAuthenticatorTests | 10 | Token refresh, config, failure callbacks |
| MIMETypeTests | 35 | All extensions, case insensitivity, URL detection |
| MultipartFormDataTests | 16 | Fields, files, encoding, binary data |
| Total | 207 |
- iOS 16+ / macOS 13+ / tvOS 16+ / watchOS 9+
- Swift 6.0+
- Zero third-party dependencies (URLSession, Foundation, CommonCrypto)
- CCSwiftAPICache -- Intelligent response caching with multiple strategies
- CCSwiftFeatureConfig -- Feature flags with targeting and A/B testing
- CCSwiftLibsDemoApp -- Demo app showcasing all three libraries
MIT