A comprehensive, thread-safe networking library for Swift applications with support for both Combine and async/await programming models.
- Dual Programming Models — Combine publishers and async/await
- Thread Safety — All operations use dedicated dispatch queues for synchronization
- Configurable Retry Logic — Pluggable
RetryHandlerprotocol for custom retry strategies - Upload Support — Single-file and multipart form data uploads with progress tracking
- Streaming — Combine and
AsyncThrowingStreambased streaming responses - Network Monitoring — Real-time connectivity and VPN detection via
NetworkMonitor - Cache Control —
CacheStrategyandCacheConfigurationfor fine-grained cache management - Error Handling —
NetworkErrorwithLocalizedErrorconformance and convenience properties - Logging — Four log levels (
none,minimal,standard,verbose) - Authentication —
HeaderHandlerbuilder for authorization, content-type, and custom headers - MIME Detection — Automatic MIME type detection from file data
- iOS 13.0+
- macOS 13.0+
- tvOS 13.0+
- watchOS 7.0+
- Swift 5.9+
Add the following dependency to your Package.swift:
dependencies: [
.package(url: "https://github.com/aspect-build/SRGenericNetworkLayer.git", from: "1.0.0")
]Or add it directly in Xcode:
- File > Add Package Dependencies
- Enter the repository URL
- Select the version you want to use
import SRNetworkManager
struct GetUsersEndpoint: NetworkRouter {
var baseURLString: String { "https://api.example.com" }
var path: String { "/users" }
var method: RequestMethod? { .get }
}let client = APIClient()
client.request(GetUsersEndpoint())
.sink(
receiveCompletion: { completion in
if case .failure(let error) = completion {
print("Error: \(error.localizedDescription)")
}
},
receiveValue: { (users: [User]) in
print("Received \(users.count) users")
}
)
.store(in: &cancellables)do {
let users: [User] = try await client.request(GetUsersEndpoint())
print("Received \(users.count) users")
} catch {
print("Error: \(error.localizedDescription)")
}let client = APIClient(
configuration: .default, // optional URLSessionConfiguration
configurationDelegate: nil, // optional URLSessionDelegate
qos: .userInitiated,
logLevel: .standard,
defaultCacheStrategy: .useProtocolCachePolicy,
decoder: JSONDecoder(),
retryHandler: DefaultRetryHandler(numberOfRetries: 3)
)Define your API endpoints with type safety.
struct CreateUserEndpoint: NetworkRouter {
struct Body: Codable {
let name: String
let email: String
}
var baseURLString: String { "https://api.example.com" }
var path: String { "/users" }
var method: RequestMethod? { .post }
var params: Body? { body }
private let body: Body
init(name: String, email: String) {
self.body = Body(name: name, email: email)
}
}let monitor = NetworkMonitor()
monitor.startMonitoring()
// Combine
monitor.status
.sink { connectivity in
switch connectivity {
case .disconnected:
print("Offline")
case .connected(let type):
print("Connected via \(type)")
}
}
.store(in: &cancellables)
// Async/Await
for await connectivity in monitor.statusStream {
print(connectivity)
}Upload a single file with automatic MIME type detection and progress tracking.
// Combine
client.uploadRequest(endpoint, withName: "photo", data: imageData) { progress in
print("Upload: \(Int(progress * 100))%")
}
.sink(
receiveCompletion: { _ in },
receiveValue: { (response: UploadResponse) in
print("Done: \(response.url)")
}
)
.store(in: &cancellables)
// Async/Await
let response: UploadResponse = try await client.uploadRequest(
endpoint, withName: "photo", data: imageData
) { progress in
print("Upload: \(Int(progress * 100))%")
}Use MultipartFormField to build requests with multiple text and file fields — similar to curl --form.
// Equivalent curl:
// curl -X POST https://api.example.com/upload \
// --form 'file=@/path/to/file.zip' \
// --form 'checksum=abc123' \
// --form 'type=document' \
// --form 'date=2025-01-01'
let fields: [MultipartFormField] = [
.file(name: "file", data: fileData, fileName: "file.zip", mimeType: "application/zip"),
.text(name: "checksum", value: "abc123"),
.text(name: "type", value: "document"),
.text(name: "date", value: "2025-01-01"),
]
// Combine
client.uploadRequest(endpoint, formFields: fields) { progress in
print("Upload: \(Int(progress * 100))%")
}
.sink(
receiveCompletion: { _ in },
receiveValue: { (response: UploadResponse) in
print("Done")
}
)
.store(in: &cancellables)
// Async/Await
let response: UploadResponse = try await client.uploadRequest(
endpoint, formFields: fields
) { progress in
print("Upload: \(Int(progress * 100))%")
}MultipartFormField supports two cases:
.text(name:value:)— a plain text field.file(name:data:fileName:mimeType:)— a file field;mimeTypeis optional and auto-detected from data whennil
client.streamRequest(StreamingEndpoint())
.sink(
receiveCompletion: { _ in print("Stream ended") },
receiveValue: { (chunk: DataChunk) in
print("Chunk: \(chunk)")
}
)
.store(in: &cancellables)for try await chunk: DataChunk in client.asyncStreamRequest(StreamingEndpoint()) {
print("Chunk: \(chunk)")
}Set the default cache strategy when initializing the client:
let client = APIClient(defaultCacheStrategy: .returnCacheDataElseLoad)Available strategies:
.useProtocolCachePolicy(default).reloadIgnoringLocalCacheData.returnCacheDataElseLoad.returnCacheDataDontLoad.reloadRevalidatingCacheData
Update at runtime:
client.updateDefaultCacheStrategy(.reloadIgnoringLocalCacheData)Configure custom URLCache capacities:
let cacheConfig = CacheConfiguration(
memoryCapacity: 20 * 1024 * 1024, // 20 MB
diskCapacity: 100 * 1024 * 1024, // 100 MB
diskPath: nil // system default
)
client.updateCacheConfiguration(cacheConfig)let client = APIClient(retryHandler: DefaultRetryHandler(numberOfRetries: 3))struct CustomRetryHandler: RetryHandler {
let numberOfRetries: Int
func shouldRetry(request: URLRequest, error: NetworkError) -> Bool {
switch error {
case .urlError(let urlError):
return urlError.code == .notConnectedToInternet ||
urlError.code == .timedOut
case .customError(let statusCode, _):
return statusCode >= 500
default:
return false
}
}
func modifyRequestForRetry(client: APIClient, request: URLRequest, error: NetworkError) -> (URLRequest, NetworkError?) {
var newRequest = request
newRequest.setValue("retry", forHTTPHeaderField: "X-Retry-Attempt")
return (newRequest, nil)
}
// Implement async variants as needed...
}HeaderHandler uses a builder pattern. Each call to build() returns the accumulated headers and resets the builder.
let headers = HeaderHandler.shared
.addAuthorizationHeader(type: .bearer(token: "your-token"))
.addContentTypeHeader(type: .applicationJson)
.addAcceptHeaders(type: .applicationJson)
.addAcceptLanguageHeaders(type: .en)
.addAcceptEncodingHeaders(type: .gzip)
.addCustomHeader(name: "X-API-Key", value: "your-api-key")
.build()
struct AuthenticatedEndpoint: NetworkRouter {
var baseURLString: String { "https://api.example.com" }
var path: String { "/protected" }
var method: RequestMethod? { .get }
var headers: [String: String]? { headers }
}NetworkError conforms to LocalizedError, so error.localizedDescription returns a meaningful message.
do {
let data: MyModel = try await client.request(endpoint)
} catch let error as NetworkError {
// Convenience properties
print(error.localizedDescription) // human-readable message
print(error.statusCode) // Int? — HTTP status for .customError
print(error.responseData) // Data? — response body for .customError
// Exhaustive matching
switch error {
case .urlError(let urlError):
if urlError.code == .notConnectedToInternet {
showOfflineMessage()
}
case .decodingError(let decodingError):
print("Decoding failed: \(decodingError)")
case .customError(let statusCode, let data):
if statusCode == 401 { handleUnauthorized() }
case .responseError(let error):
print("Response error: \(error)")
case .unknown:
print("Unknown error")
}
}Cancel all active requests and clear the retry queue:
client.cancelAllRequests()Update the session configuration at runtime (invalidates existing sessions by default):
let newConfig = URLSessionConfiguration.default
newConfig.timeoutIntervalForRequest = 30
client.updateConfiguration(newConfig)let vpnChecker = VPNChecker()
if vpnChecker.isVPNActive() {
print("VPN is active")
}let client = APIClient(logLevel: .verbose) // .none, .minimal, .standard, .verbose#if DEBUG
let logLevel: LogLevel = .verbose
let retryHandler = DefaultRetryHandler(numberOfRetries: 3)
#else
let logLevel: LogLevel = .none
let retryHandler = DefaultRetryHandler(numberOfRetries: 1)
#endif
let client = APIClient(
logLevel: logLevel,
retryHandler: retryHandler
)All operations are thread-safe:
- APIClient — Dedicated
DispatchQueuewith barrier flags for read/write synchronization - NetworkMonitor — Thread-safe status updates and async continuation management
- HeaderHandler — Synchronized header operations with automatic reset on
build() - UploadProgressDelegate — Thread-safe progress reporting
| Type | Description |
|---|---|
APIClient |
Main client for network requests |
NetworkRouter |
Protocol for defining API endpoints |
NetworkError |
Error enum with LocalizedError conformance |
MultipartFormField |
Enum for multipart form text and file fields |
RetryHandler |
Protocol for custom retry logic |
CacheStrategy |
Enum mapping to URLRequest.CachePolicy |
CacheConfiguration |
Struct for URLCache memory/disk capacities |
NetworkMonitor |
Real-time network connectivity monitoring |
VPNChecker |
VPN connection detection |
HeaderHandler |
Builder for HTTP headers |
GET, POST, PUT, PATCH, DELETE, HEAD, TRACE
applicationJson, urlEncoded, formData
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
This project is licensed under the MIT License - see the LICENSE file for details.