Skip to content

pelagornis/swift-network

Repository files navigation

Swift NetworkKit

Official Swift Platform License

A modern, protocol-oriented Swift networking library with enterprise-grade features.

Features

  • 🚀 Protocol-Oriented Design - Built with Swift protocols for maximum flexibility
  • 🔌 Plugin System - Extensible plugin architecture for logging, caching, and more
  • 🛡️ Enterprise Features - Retry policies, circuit breakers, rate limiting, and security
  • 📊 Metrics & Monitoring - Built-in metrics collection and monitoring
  • 🔄 Request Modifiers - Chainable request modifications
  • 🎯 Type Safety - Full type safety with generics and protocols
  • Async/Await - Modern Swift concurrency support
  • 🧪 Testable - Designed for easy testing and mocking
  • 📡 Network Connectivity Monitoring - Real-time network status monitoring using Network Framework (iOS 12.0+, macOS 10.14+)

Installation

Swift Package Manager

Add the following to your Package.swift:

dependencies: [
    .package(url: "https://github.com/pelagornis/swift-network.git", from: "vTag")
]

Or add it directly in Xcode:

  1. File → Add Package Dependencies
  2. Enter the repository URL
  3. Select the version you want to use

Documentation

The documentation for releases and main are available here:

Quick Start

Basic Usage

import NetworkKit

// Define your endpoint using SwiftUI-style DSL
enum UserEndpoint: Endpoint {
    case getUsers
    case getUser(id: Int)
    case createUser(name: String, email: String)
    
    var body: HTTPEndpoint {
        switch self {
        case .getUsers:
            return HTTP {
                BaseURL("https://api.example.com")
                Path("/users")
                Method(.get)
                HTTPTask(.requestPlain)
                Headers([.accept("application/json")])
                Timeout(30.0)
            }
        case .getUser(let id):
            return HTTP {
                BaseURL("https://api.example.com")
                Path("/users/\(id)")
                Method(.get)
                HTTPTask(.requestPlain)
                Headers([.accept("application/json")])
            }
        case .createUser(let name, let email):
            let user = CreateUserRequest(name: name, email: email)
            return HTTP {
                BaseURL("https://api.example.com")
                Path("/users")
                Method(.post)
                HTTPTask(.requestJSON(user))
                Headers([.contentType("application/json")])
            }
        }
    }
}

// Create a network provider
let provider = NetworkProvider<UserEndpoint>()

// Make a request
do {
    let users: [User] = try await provider.request(.getUsers, as: [User].self)
    print("Users: \(users)")
} catch {
    print("Error: \(error)")
}

With Enterprise Features

import NetworkKit

// Configure enterprise features
let retryPolicy = ExponentialBackoffRetryPolicy(maxAttempts: 3)
let rateLimiter = TokenBucketRateLimiter(config: RateLimitConfig(maxRequests: 100, timeWindow: 60))
let circuitBreaker = DefaultCircuitBreaker(config: CircuitBreakerConfig())
let cacheManager = MemoryCacheManager()
let metricsCollector = DefaultMetricsCollector()

// Create enterprise-ready provider
let provider = NetworkProvider<UserEndpoint>(
    retryPolicy: retryPolicy,
    rateLimiter: rateLimiter,
    circuitBreaker: circuitBreaker,
    cacheManager: cacheManager,
    metricsCollector: metricsCollector
)

// Make a request with all enterprise features
do {
    let users: [User] = try await provider.request(.getUsers, as: [User].self)
    print("Users: \(users)")

    // Access metrics
    if let metrics = provider.getMetrics() {
        print("Request count: \(metrics.requestCount)")
        print("Average response time: \(metrics.averageResponseTime)")
    }
} catch {
    print("Error: \(error)")
}

Core Concepts

Endpoints

Endpoints define your API endpoints using the Endpoint protocol with a SwiftUI-style DSL:

Basic Endpoint Definition

enum UserEndpoint: Endpoint {
    case getUsers
    case getUser(id: Int)
    
    var body: HTTPEndpoint {
        switch self {
        case .getUsers:
            return HTTP {
                BaseURL("https://api.example.com")
                Path("/users")
                Method(.get)
                HTTPTask(.requestPlain)
                Headers([.accept("application/json")])
            }
        case .getUser(let id):
            return HTTP {
                BaseURL("https://api.example.com")
                Path("/users/\(id)")
                Method(.get)
                HTTPTask(.requestPlain)
            }
        }
    }
}

Reusable Base Endpoint

For better reusability, you can define a base endpoint with common settings:

enum UserEndpoint: Endpoint {
    case getUsers
    case getUser(id: Int)
    case createUser(name: String, email: String)
    
    // Base endpoint with common settings
    private static var baseEndpoint: HTTPEndpoint {
        HTTP {
            BaseURL("https://api.example.com")
            Headers([
                .accept("application/json"),
                .authorization("Bearer token")
            ])
            Timeout(30.0)
        }
    }
    
    var body: HTTPEndpoint {
        switch self {
        case .getUsers:
            return HTTP(base: Self.baseEndpoint) {
                Path("/users")
                Method(.get)
                HTTPTask(.requestPlain)
            }
        case .getUser(let id):
            return HTTP(base: Self.baseEndpoint) {
                Path("/users/\(id)")
                Method(.get)
                HTTPTask(.requestPlain)
            }
        case .createUser(let name, let email):
            let user = CreateUserRequest(name: name, email: email)
            return HTTP(base: Self.baseEndpoint) {
                Path("/users")
                Method(.post)
                HTTPTask(.requestJSON(user))
                Headers([.contentType("application/json")])
            }
        }
    }
}

Request with Parameters

enum SearchEndpoint: Endpoint {
    case search(query: String, page: Int = 1, perPage: Int = 20)
    
    var body: HTTPEndpoint {
        switch self {
        case .search(let query, let page, let perPage):
            let parameters = [
                "q": query,
                "page": "\(page)",
                "per_page": "\(perPage)"
            ]
            return HTTP {
                BaseURL("https://api.example.com")
                Path("/search")
                Method(.get)
                HTTPTask(.requestParameters(parameters, encoding: .url))
            }
        }
    }
}

Request Modifiers

Modify requests dynamically:

let modifiers: [RequestModifier] = [
    HeaderModifier(key: "Authorization", value: "Bearer token"),
    TimeoutModifier(timeout: 60),
    CachePolicyModifier(policy: .reloadIgnoringLocalCacheData)
]

let users: [User] = try await provider.request(
    .getUsers,
    as: [User].self,
    modifiers: modifiers
)

Plugins

Extend functionality with plugins:

// Logging plugin
let loggingPlugin = LoggingPlugin(logger: ConsoleLogger(level: .info))

// Rate limiting plugin
let rateLimitingPlugin = RateLimitingPlugin(rateLimiter: rateLimiter)

// Circuit breaker plugin
let circuitBreakerPlugin = CircuitBreakerPlugin(circuitBreaker: circuitBreaker)

let provider = NetworkProvider<UserEndpoint>(
    plugins: [loggingPlugin, rateLimitingPlugin, circuitBreakerPlugin]
)

Status Code Handling

NetworkKit automatically handles HTTP status codes:

  • 2xx (200-299): Success - Response is decoded and returned
  • Other status codes: Error - Throws NetworkError.serverError with status code and response data

NetworkKit provides a comprehensive Http.StatusCode enum with all standard HTTP status codes for type-safe status code handling.

Using Status Code Enum

import NetworkKit

// Create status code from integer
let statusCode = Http.StatusCode(rawValue: 404)
print(statusCode) // .notFound
print(statusCode.rawValue) // 404
print(statusCode.description) // "Not Found"

// Check status code category
if statusCode.isClientError {
    print("This is a client error")
}

// Create from HTTPURLResponse
if let httpResponse = response as? HTTPURLResponse {
    let statusCode = Http.StatusCode(from: httpResponse)
    print("Status: \(statusCode.description)")
}

Basic Status Code Handling

do {
    let users: [User] = try await provider.request(.getUsers, as: [User].self)
    // Success - status code is 2xx
} catch NetworkError.serverError(let statusCode, let data) {
    // Convert integer status code to enum
    let httpStatusCode = Http.StatusCode(rawValue: statusCode)

    switch httpStatusCode {
    case .badRequest:
        print("Bad Request")
    case .unauthorized:
        print("Unauthorized")
    case .notFound:
        print("Not Found")
    case .internalServerError:
        print("Internal Server Error")
    case .serviceUnavailable:
        print("Service Unavailable")
    default:
        print("Server error: \(httpStatusCode.description) (\(statusCode))")
    }

    // Access response data if needed
    if let errorMessage = String(data: data, encoding: .utf8) {
        print("Error message: \(errorMessage)")
    }
} catch {
    print("Other error: \(error)")
}

Status Code Categories

let statusCode = Http.StatusCode(rawValue: 200)

// Check status code category
if statusCode.isSuccess {
    print("Request succeeded")
}

if statusCode.isClientError {
    print("Client error occurred")
}

if statusCode.isServerError {
    print("Server error occurred")
}

if statusCode.isRedirection {
    print("Redirection required")
}

if statusCode.isInformational {
    print("Informational response")
}

Custom Status Code Handling

You can create a custom ResponseHandler to handle specific status codes differently using the StatusCode enum:

struct CustomResponseHandler: ResponseHandler {
    let decoder: JSONDecoder

    init(decoder: JSONDecoder = JSONDecoder()) {
        self.decoder = decoder
    }

    func handle<T: Decodable>(_ data: Data, response: URLResponse, as type: T.Type) throws -> T {
        guard let httpResponse = response as? HTTPURLResponse else {
            throw NetworkError.unknown
        }

        let statusCode = Http.StatusCode(from: httpResponse)

        switch statusCode {
        case .ok, .created, .accepted, .noContent:
            // Success - decode and return
            return try decoder.decode(type, from: data)
        case .unauthorized:
            // Unauthorized - throw specific error
            throw NetworkError.serverError(statusCode: statusCode.rawValue, data: data)
        case .notFound:
            // Not Found - return empty result or throw
            throw NetworkError.serverError(statusCode: statusCode.rawValue, data: data)
        case .tooManyRequests:
            // Rate limited - special handling
            throw NetworkError.rateLimitExceeded
        default:
            if statusCode.isServerError {
                // Retry server errors
                throw NetworkError.serverError(statusCode: statusCode.rawValue, data: data)
            } else {
                // Other client errors
                throw NetworkError.serverError(statusCode: statusCode.rawValue, data: data)
            }
        }
    }
}

// Use custom response handler
let customHandler = CustomResponseHandler()
let provider = NetworkProvider<UserEndpoint>(responseHandler: customHandler)

Status Code Validation

The default ResponseHandler validates that status codes are in the 200-299 range. You can customize this behavior using the StatusCode enum:

struct PermissiveResponseHandler: ResponseHandler {
    let decoder: JSONDecoder

    func handle<T: Decodable>(_ data: Data, response: URLResponse, as type: T.Type) throws -> T {
        guard let httpResponse = response as? HTTPURLResponse else {
            throw NetworkError.unknown
        }

        let statusCode = Http.StatusCode(from: httpResponse)

        // Allow 2xx and 3xx status codes
        guard statusCode.isSuccess || statusCode.isRedirection else {
            throw NetworkError.serverError(statusCode: statusCode.rawValue, data: data)
        }

        return try decoder.decode(type, from: data)
    }
}

Available Status Codes

The Http.StatusCode enum includes all standard HTTP status codes:

  • 1xx Informational: continue, switchingProtocols, processing, earlyHints
  • 2xx Success: ok, created, accepted, noContent, partialContent, etc.
  • 3xx Redirection: movedPermanently, found, seeOther, notModified, temporaryRedirect, etc.
  • 4xx Client Error: badRequest, unauthorized, forbidden, notFound, methodNotAllowed, tooManyRequests, etc.
  • 5xx Server Error: internalServerError, badGateway, serviceUnavailable, gatewayTimeout, etc.

For a complete list, see the Http.StatusCode enum definition in the source code.

Enterprise Features

Network Connectivity Monitoring

NetworkKit provides real-time network connectivity monitoring using Apple's Network Framework. This allows your app to adapt its behavior based on network conditions and connection type.

Basic Usage

import NetworkKit

// Create provider with connectivity monitoring enabled
let provider = NetworkProvider<UserEndpoint>(checkConnectivity: true)

// Check current connection status
if let isConnected = await provider.isConnected() {
    if isConnected {
        print("Device is connected to the internet")
    } else {
        print("No internet connection")
    }
}

// Get connection type
if let connectionType = await provider.getConnectionType() {
    switch connectionType {
    case .wifi:
        print("Connected via WiFi")
    case .cellular:
        print("Connected via Cellular")
    case .ethernet:
        print("Connected via Ethernet")
    default:
        print("Connected via \(connectionType)")
    }
}

// Get detailed network status
if let status = await provider.getNetworkStatus() {
    switch status {
    case .connected(let type):
        print("Connected via \(type)")
    case .disconnected:
        print("No connection")
    case .connecting:
        print("Connecting...")
    case .requiresConnection:
        print("Connection required")
    }
}

Automatic Connectivity Checking

When connectivity checking is enabled, NetworkProvider automatically checks the connection before making requests. If no connection is available, it will:

  1. Return cached data if available
  2. Throw NetworkError.noConnection if no cache is available
// Provider with connectivity checking and caching
let cacheManager = MemoryCacheManager()
let provider = NetworkProvider<UserEndpoint>(
    cacheManager: cacheManager,
    checkConnectivity: true
)

do {
    // If offline, will return cached data or throw noConnection error
    let users: [User] = try await provider.request(.getUsers, as: [User].self)
} catch NetworkError.noConnection {
    print("No internet connection and no cached data available")
}

Custom Network Monitor

You can provide a custom network monitor for specific use cases:

// Monitor only WiFi connections
let wifiMonitor = WiFiNetworkMonitor()
wifiMonitor.startMonitoring()

// Monitor only cellular connections
let cellularMonitor = CellularNetworkMonitor()
cellularMonitor.startMonitoring()

// Use custom monitor with provider
let provider = NetworkProvider<UserEndpoint>(
    networkMonitor: wifiMonitor
)

// Observe network status changes
Task {
    for await status in wifiMonitor.statusUpdates {
        switch status {
        case .connected(let type):
            print("Network connected: \(type)")
        case .disconnected:
            print("Network disconnected")
        default:
            break
        }
    }
}

Standalone Network Monitoring

You can also use NetworkMonitor independently:

let monitor = DefaultNetworkMonitor()
monitor.startMonitoring()

// Check current status
let status = await monitor.currentStatus

// Observe status changes
Task {
    for await status in monitor.statusUpdates {
        print("Network status changed: \(status)")
    }
}

// Check if connected
let isConnected = await monitor.isConnected

// Get connection type
if let type = await monitor.connectionType {
    print("Connection type: \(type)")
}

Retry Policies

// Exponential backoff
let exponentialRetry = ExponentialBackoffRetryPolicy(maxAttempts: 3)

// Fixed delay
let fixedRetry = FixedDelayRetryPolicy(maxAttempts: 3, delay: 1.0)

// Custom retry
let customRetry = CustomRetryPolicy { error, attempt, request in
    // Custom retry logic
    return attempt < 3 && error is NetworkError.serverError
}

Circuit Breaker

let config = CircuitBreakerConfig(
    failureThreshold: 5,
    recoveryTimeout: 30,
    expectedFailureRate: 0.5
)

let circuitBreaker = DefaultCircuitBreaker(config: config)

// Check state
if let state = provider.getCircuitBreakerState() {
    switch state {
    case .closed:
        print("Circuit breaker is closed - requests allowed")
    case .open:
        print("Circuit breaker is open - requests blocked")
    case .halfOpen:
        print("Circuit breaker is half-open - limited requests allowed")
    }
}

Rate Limiting

// Token bucket rate limiter
let tokenBucket = TokenBucketRateLimiter(
    config: RateLimitConfig(maxRequests: 100, timeWindow: 60, burstSize: 10)
)

// Sliding window rate limiter
let slidingWindow = SlidingWindowRateLimiter(
    config: RateLimitConfig(maxRequests: 100, timeWindow: 60)
)

// Endpoint-specific rate limiting
let endpointConfigs = [
    "api.example.com-/users-GET": RateLimitConfig(maxRequests: 50, timeWindow: 60),
    "api.example.com-/posts-GET": RateLimitConfig(maxRequests: 200, timeWindow: 60)
]

let endpointLimiter = EndpointSpecificRateLimiter(
    defaultConfig: RateLimitConfig(maxRequests: 100, timeWindow: 60),
    endpointConfigs: endpointConfigs
)

Security

let securityManager = DefaultSecurityManager()

// Add certificate pinning
if let certificate = loadCertificate() {
    securityManager.addCertificatePinning(certificate, for: "api.example.com")
}

// Configure SSL validation
securityManager.allowInvalidCertificates = false

Caching

let cacheManager = MemoryCacheManager()

// Cache with expiration
cacheManager.set(user, for: "user:123", expiration: 300) // 5 minutes

// Get cached data
if let cachedUser: User = cacheManager.get(for: "user:123", as: User.self) {
    print("Cached user: \(cachedUser)")
}

// Clear cache
cacheManager.clear()

Metrics

let metricsCollector = DefaultMetricsCollector()

// Get metrics
if let metrics = provider.getMetrics() {
    print("Total requests: \(metrics.requestCount)")
    print("Successful requests: \(metrics.successCount)")
    print("Failed requests: \(metrics.failureCount)")
    print("Average response time: \(metrics.averageResponseTime)")
    print("Cache hit rate: \(metrics.cacheHitRate)")
}

// Reset metrics
provider.resetMetrics()

Testing

The library is designed for easy testing:

import XCTest
@testable import NetworkKit

class NetworkTests: XCTestCase {
    func testNetworkRequest() async throws {
        // Create mock session
        let mockSession = MockSession()
        mockSession.mockResponse = (Data(), URLResponse())

        // Create provider with mock session
        let provider = NetworkProvider<UserEndpoint>(session: mockSession)

        // Test request
        let users: [User] = try await provider.request(.getUsers, as: [User].self)
        XCTAssertNotNil(users)
    }
}

Architecture

The library follows a protocol-oriented design with clear separation of concerns:

  • Endpoint - Defines API endpoints using SwiftUI-style DSL with body property
  • HTTPEndpoint - Concrete endpoint implementation that stores request information
  • HTTPBuilder - Result builder for declarative endpoint definition
  • NetworkProvider - Main networking class with enterprise features
  • Session - Handles actual network requests
  • NetworkPlugin - Extensible plugin system
  • RequestModifier - Chainable request modifications
  • ResponseHandler - Handles response processing
  • CacheManager - Caching functionality
  • NetworkLogger - Logging system

DSL Components

The HTTP DSL provides the following components:

  • BaseURL - Sets the base URL for the API
  • Path - Defines the endpoint path
  • Method - Specifies the HTTP method (GET, POST, PUT, DELETE, etc.)
  • HTTPTask - Defines the request task (plain, JSON, parameters, multipart, etc.)
  • Headers - Sets HTTP headers
  • Timeout - Sets custom timeout for the request
  • HTTP(base:) - Inherits properties from a base endpoint for reusability

Contributing

Contributions are welcome! Please read our Contributing Guide for details.

License

swift-network is under MIT license. See the LICENSE file for more info.

About

Swift Networking Library

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages