Skip to content

mindobix/CCSwiftNetworking

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CCSwiftNetworking

A production-ready Swift networking library built on URLSession. Zero third-party dependencies -- 100% Apple frameworks.

Features

  • 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 AuthInterceptor in 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 Sendable conformance throughout

Setup

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.

Usage

Creating the Client

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
}

Making Requests

// 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")
])

Handling Responses

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")")
}

Per-Request Timeout

// 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)

Chaining Dependent Calls (flatMap)

let result: APIResponse<[Order]> = await client.get("/users/me")
    .flatMap { user in
        await client.get("/users/\(user.id)/orders")
    }

Callback Chaining

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)
    }

Data Transformation

let nameResponse: APIResponse<String> = await client.get("/users/me")
    .map { (user: User) in user.name }

Network Error Classification

case .networkError(let error, let kind):
    switch kind {
    case .noConnection: showOfflineBanner()
    case .timeout:      showRetryButton()
    case .dnsFailure:   showServerUnreachable()
    case .sslError:     showSecurityWarning()
    case .unknown:      showGenericError()
    }

Authentication

// 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
}

Token Refresh Flow

Request -> 401 -> TokenAuthenticator.refreshIfNeeded()
                   |
                   +-- Already refreshing? -> Wait for result
                   |
                   +-- Acquire lock -> Call refresh endpoint
                       |
                       +-- Success -> Update tokens -> Retry original request
                       |
                       +-- Failure -> onAuthenticationFailed()

File Uploads

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)

File Upload from URL

let multipart = MultipartFormData()
try multipart.addFile(url: fileURL, name: "document") // Auto-detects MIME type
try multipart.addFiles(urls: [file1, file2], name: "attachments")

Certificate Pinning

let client = NetworkingClient(baseURL: "https://api.example.com") { config in
    config.certificatePins = [
        "api.example.com": ["sha256/AAAA=", "sha256/BBBB="] // Primary + backup
    ]
}

Custom Interceptors

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()]
}

Builder Options

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

Architecture

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

Tests

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

Requirements

  • iOS 16+ / macOS 13+ / tvOS 16+ / watchOS 9+
  • Swift 6.0+
  • Zero third-party dependencies (URLSession, Foundation, CommonCrypto)

Related Libraries

  • CCSwiftAPICache -- Intelligent response caching with multiple strategies
  • CCSwiftFeatureConfig -- Feature flags with targeting and A/B testing
  • CCSwiftLibsDemoApp -- Demo app showcasing all three libraries

License

MIT

About

Swift 6 networking library with zero third-party dependencies

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages