A modern, protocol-oriented Swift networking library with enterprise-grade 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+)
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:
- File → Add Package Dependencies
- Enter the repository URL
- Select the version you want to use
The documentation for releases and main are available here:
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)")
}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)")
}Endpoints define your API endpoints using the Endpoint protocol with a SwiftUI-style DSL:
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)
}
}
}
}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")])
}
}
}
}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))
}
}
}
}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
)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]
)NetworkKit automatically handles HTTP status codes:
- 2xx (200-299): Success - Response is decoded and returned
- Other status codes: Error - Throws
NetworkError.serverErrorwith status code and response data
NetworkKit provides a comprehensive Http.StatusCode enum with all standard HTTP status codes for type-safe status code handling.
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)")
}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)")
}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")
}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)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)
}
}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.
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.
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")
}
}When connectivity checking is enabled, NetworkProvider automatically checks the connection before making requests. If no connection is available, it will:
- Return cached data if available
- Throw
NetworkError.noConnectionif 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")
}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
}
}
}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)")
}// 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
}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")
}
}// 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
)let securityManager = DefaultSecurityManager()
// Add certificate pinning
if let certificate = loadCertificate() {
securityManager.addCertificatePinning(certificate, for: "api.example.com")
}
// Configure SSL validation
securityManager.allowInvalidCertificates = falselet 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()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()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)
}
}The library follows a protocol-oriented design with clear separation of concerns:
Endpoint- Defines API endpoints using SwiftUI-style DSL withbodypropertyHTTPEndpoint- Concrete endpoint implementation that stores request informationHTTPBuilder- Result builder for declarative endpoint definitionNetworkProvider- Main networking class with enterprise featuresSession- Handles actual network requestsNetworkPlugin- Extensible plugin systemRequestModifier- Chainable request modificationsResponseHandler- Handles response processingCacheManager- Caching functionalityNetworkLogger- Logging system
The HTTP DSL provides the following components:
BaseURL- Sets the base URL for the APIPath- Defines the endpoint pathMethod- Specifies the HTTP method (GET, POST, PUT, DELETE, etc.)HTTPTask- Defines the request task (plain, JSON, parameters, multipart, etc.)Headers- Sets HTTP headersTimeout- Sets custom timeout for the requestHTTP(base:)- Inherits properties from a base endpoint for reusability
Contributions are welcome! Please read our Contributing Guide for details.
swift-network is under MIT license. See the LICENSE file for more info.