Lightweight, actor-based Swift networking SDK
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.
