Lightweight, actor-based Swift networking SDK
Swift Platforms SPM License CI codecov
Typed JSON requests · Auth strategies · Retry with backoff · Interceptor pipeline · SwiftUI image loading · Zero dependencies
- Actor-based —
NexioClientis a Swift actor; zero data-race risk, safe to call from any concurrency context - Typed requests —
get,post,put,patch,deletereturn decodedDecodablevalues directly - Type-safe endpoints —
Endpointprotocol for grouping request details in reusable structs - Auth strategies — bearer token, API key, custom headers, or dynamic provider (OAuth refresh)
- Interceptor pipeline — adapt requests and retry failures with full control
- Retry with backoff — none, linear, or exponential backoff; configurable per policy
- SwiftUI image loading —
NexioImagedrop-in forAsyncImagewithURLCache-backed caching and prefetch - Structured errors —
NexioErrorcovers network, auth, 4xx/5xx, and decoding failures - Zero dependencies — pure Swift, built on
URLSession
// Package.swift dependencies: [ .package(url: "https://github.com/ANSCoder/Nexio", from: "1.0.0") ], targets: [ .target(name: "YourApp", dependencies: [ .product(name: "Nexio", package: "Nexio") ]) ]
Or add it in Xcode: File → Add Package Dependencies, paste the repo URL.
// AppDelegate / App.init var config = NexioConfig() config.baseURL = URL(string: "https://api.example.com") config.timeout = 15 config.retry = .standard // 3 attempts, exponential backoff config.logLevel = .errors await NexioClient.shared.configure(config) await NexioClient.shared.setAuth(.bearer("your-token"))
// GET — decodes JSON automatically let users: [User] = try await NexioClient.shared.get("/users") // POST with body let body = CreateUserRequest(name: "Alice") let created: User = try await NexioClient.shared.post("/users", body: body) // PUT / PATCH / DELETE let updated: User = try await NexioClient.shared.put("/users/42", body: changes) try await NexioClient.shared.delete("/users/42") // Absolute URLs work too (ignores baseURL) let user: User = try await NexioClient.shared.get("https://api.example.com/users/1")
// Shorthand for NexioClient.shared.get / .post let users: [User] = try await nexioGet("/users") let created: User = try await nexioPost("/users", body: newUser)
// Static bearer token await NexioClient.shared.setAuth(.bearer("jwt-token")) // API key in a custom header await NexioClient.shared.setAuth(.apiKey(header: "X-Api-Key", value: "secret")) // Arbitrary headers await NexioClient.shared.setAuth(.custom(["X-Tenant-ID": "acme", "X-Version": "2"])) // Dynamic token — closure called before every request (ideal for OAuth refresh) let authInterceptor = AuthInterceptor { await TokenStore.shared.currentToken() // returns AuthStrategy } await NexioClient.shared.addInterceptor(authInterceptor)
struct PublicEndpoint: Endpoint { var baseURL: URL { URL(string: "https://api.example.com")! } var path: String { "/status" } var method: HTTPMethod { .get } var auth: AuthStrategy? { .some(.none) } // skip global auth for this request }
Group URL, method, query params, headers, and body in one reusable struct:
struct GetUser: Endpoint { let id: Int var baseURL: URL { URL(string: "https://api.example.com")! } var path: String { "/users/\(id)" } var method: HTTPMethod { .get } } struct SearchUsers: Endpoint { let query: String var baseURL: URL { URL(string: "https://api.example.com")! } var path: String { "/users/search" } var method: HTTPMethod { .get } var queryItems: [URLQueryItem] { [URLQueryItem(name: "q", value: query)] } } let user: User = try await NexioClient.shared.request(GetUser(id: 42)) let results: [User] = try await NexioClient.shared.request(SearchUsers(query: "alice"))
// Via config (recommended — applied automatically) config.retry = .standard // 3 retries, exponential backoff config.retry = RetryPolicy(maxAttempts: 5, backoff: .linear(seconds: 2)) // custom // Via interceptor (for per-client or programmatic control) await NexioClient.shared.addInterceptor( RetryInterceptor(policy: .standard) )
Retried automatically on: .noInternet, .timeout, and 5xx serverError.
Not retried: 4xx errors (client errors are not transient).
Implement Interceptor to hook into every request:
struct LoggingInterceptor: Interceptor { func adapt(_ request: URLRequest, for session: URLSession) async throws -> URLRequest { print("→ \(request.httpMethod ?? "")\(request.url?.absoluteString ?? "")") return request } func retry(_ request: URLRequest, dueTo error: NexioError, attempt: Int) async -> Bool { false } } await NexioClient.shared.addInterceptor(LoggingInterceptor())
Interceptors run in insertion order during adapt, and reverse order during retry.
All errors are typed as NexioError:
do { let user: User = try await NexioClient.shared.get("/users/1") } catch NexioError.unauthorized { // Redirect to login } catch NexioError.notFound { // Show 404 UI } catch NexioError.noInternet { // Show offline banner } catch NexioError.serverError(let statusCode, let data) { // Handle 5xx } catch NexioError.decodingFailed(let underlying, let data) { // Log raw response for debugging print(String(data: data, encoding: .utf8) ?? "") } catch NexioError.invalidURL(let string) { // Bad URL at call site }
NexioImage is a drop-in replacement for AsyncImage with transparent URLCache caching (50 MB memory / 200 MB disk by default):
// Default — gray placeholder, photo icon on failure NexioImage("https://cdn.example.com/photo.jpg") .frame(width: 100, height: 100) .clipShape(Circle()) // Custom placeholder and failure views NexioImage( "https://cdn.example.com/photo.jpg", placeholder: { ProgressView() }, failureImage: { Image(systemName: "person.crop.circle.fill") } ) // Prefetch a list for smoother scroll performance let urls = items.compactMap { URL(string: 0ドル.imageURL) } await ImageLoader.shared.prefetch(urls) // Clear cache await ImageLoader.shared.clearCache()
NexioClient is a Swift actor — all state mutations are serialized automatically with no extra effort. In-flight network I/O runs concurrently through URLSession's connection pool:
// Three requests in flight simultaneously async let users: [User] = NexioClient.shared.get("/users") async let posts: [Post] = NexioClient.shared.get("/posts") async let comments: [Comment] = NexioClient.shared.get("/comments") let (u, p, c) = try await (users, posts, comments)
Actor isolation serializes only the microsecond-scale bookkeeping (building requests, applying headers, decoding JSON). Network round-trips never block other callers.
Inject a custom URLProtocol subclass via NexioConfig.protocolClasses to stub responses without hitting the network:
var config = NexioConfig() config.protocolClasses = [MockURLProtocol.self] await client.configure(config)
| Platform | Minimum |
|---|---|
| iOS | 16.0 |
| macOS | 13.0 |
| watchOS | 9.0 |
| tvOS | 16.0 |
Swift 6.2+ · Zero external dependencies
Nexio is released under the MIT license. See LICENSE for details.