SwiftNetworking

1.0.0

AndyHeardApps/SwiftNetworking

What's New

1.0.0

2026-06-05T10:45:25Z

Initial release

SwiftNetworking

Swift Versions Platforms Documentation Testing Documentation GitHub release SPM compatible License

A declarative, SwiftUI-style HTTP networking library for Swift. Define requests as composable types, configure clients once, and let the type system carry your decoded responses.


Table of Contents


Installation

dependencies: [
    .package(url: "https://github.com/AndyHeardApps/SwiftNetworking", from: "1.0.0")
]

Add "SwiftNetworking" to your target's dependencies.


Quick Start

import SwiftNetworking

// 1. Define a request
struct GetUser: Request {
    let id: Int

    var body: some Request<User> {
        Endpoint()
            .path {
                "users"
                id
            }
            .method(.get)
            .decode(User.self)
    }
}

// 2. Build a client
let client = SessionClient(URLSessionNetworkSession())
    .baseURL(#url("https://api.example.com"))

// 3. Send it
let user = try await client.send(GetUser(id: 42)).content

Defining Requests

Conform to Request and implement a body property that chains modifiers on Endpoint(). The shape mirrors SwiftUI's View protocol.

struct CreatePost: Request {
    let title: String
    let content: String

    var body: some Request<Post> {
        Endpoint()
            .method(.post)
            .path("posts")
            .body(["title": title, "content": content])
            .decode(Post.self)
    }
}
struct DeletePost: Request {
    let id: Int

    var body: some Request<Void> {
        Endpoint()
            .method(.delete)
            .path {
                "posts"
                id
            }
            .decodeVoid()
    }
}

Nesting requests

Any request can delegate to another:

struct PutPost: Request {
    let title: String
    let content: String

    var body: some Request<Post> {
        CreatePost(
            title: title,
            content: content
        )
        .method(.put)
    }
}

Response Types — Important

Always annotate body with some Request<YourType> when you expect a decoded response. This is not compiler-enforced — omitting the type parameter silently discards the decoded type and Client.send(_:) returns Data instead.

// Correct: the decoded User flows through to the call site
struct GetUser: Request {
    var body: some Request<User> {
        Endpoint()
            .path("users/1")
            .decode(User.self)
    }
}

// Wrong: Output is lost, so send(_:) returns Data, not User
struct GetUser: Request {
    var body: some Request {
        Endpoint()
            .path("users/1")
            .decode(User.self)
    }
}

The rule is simple: if you call .decode(T.self), write some Request<T> as the return type.

For requests that return nothing, use some Request<Void> with .decodeVoid(). For raw bytes, omit the decode modifier and write some Request<Data> (or just some Request — the default Output is Data).


Request Modifiers

Modifiers are chained on Endpoint() (or any Request) to describe the full HTTP interaction.

URL & method

Endpoint()
    .baseURL(#url("https://api.example.com"))
    .path {
        "users"
        id
        "posts"
    }
    .method(.get)

Headers

Endpoint()
    .headers {
        Headers()
            .accept(.json)
            .contentType(.json)
    }
    .headers(["X-App-Version": 2])

Query parameters

Endpoint()
    .path("search")
    .query([
        "query": term,
        "limit": 20,
    ])

Request body

// Encodable model
Endpoint().body(newPost)

// JSON dictionary
Endpoint().body(["title": "Hello", "body": "World"])

// Multipart form data
Endpoint().body {
    FormData.Item(name: "title", "My Photo")
    FormData.Item(name: "file", fileURL: imageURL, mimeType: .imageJPEG)
}

// Form URL-encoded
Endpoint().body(FormURLEncoded(["username": "alice", "password": "secret"]))

// Upload from disk (enables upload task with progress)
Endpoint().upload(file: videoURL)

Decoding

Endpoint().decode(User.self)           // Decodable → User
Endpoint().decodeOptional(User.self)   // Decodable → User? (200 or 204)
Endpoint().decodeVoid()                // ignore body
Endpoint().decodeString()              // String (UTF-8)

Caching & timeouts

Endpoint()
    .cachePolicy(.reloadIgnoringLocalCacheData)
    .timeout(.seconds(30))
    .resourceTimeout(.seconds(300))

Status code assertion

Endpoint().response(.data.assertSuccess())              // throws on non-2xx
Endpoint().response(.data.assert(statusCode: .ok))      // throws unless exactly 200
Endpoint().response(.data.assert(statusCodeIn: 200...299))

Sending Requests

Create a SessionClient and use send(_:) for a direct response or task(_:) when you need progress or cancellation.

let client = SessionClient(URLSessionNetworkSession())
    .baseURL(#url("https://api.example.com"))

send(_:) — one-shot

let user = try await client.send(GetUser(id: 1)).content

task(_:) — in-flight control

let operation = client.task(GetUser(id: 1))

// Observe progress (useful for uploads/downloads)
for await progress in operation.progress {
    progressBar.value = progress.fractionCompleted
}

// Cancel early
operation.cancel()

// Await the value
let user = try await operation.value.content

Downloads

// Process a temporary file (deleted after the closure returns)
let byteCount = try await client.download(DownloadReport(id: 99)) { fileURL in
    try Data(contentsOf: fileURL).count
}

// Or move to a specific destination
let savedURL = try await client.download(DownloadReport(id: 99), to: destinationURL).content

// Track progress with a download task
let operation = client.downloadTask(DownloadReport(id: 99), to: destinationURL)
for await progress in operation.progress {
    print(progress.fractionCompleted)
}
let fileURL = try await operation.value.content

Configuration

Configure a client once with defaults that apply to every request it sends. Request-level modifiers override client defaults.

let client = SessionClient(URLSessionNetworkSession())
    .baseURL(#url("https://api.example.com"))
    .defaultHeaders(Headers().accept(.json))
    .timeout(.seconds(30))
    .cachePolicy(.reloadIgnoringLocalCacheData)
    .redirects(.follow)

Custom configuration keys

extension ConfigurationValues {
    @ConfigEntry var apiVersion: String = "v1"
}

let client = SessionClient(URLSessionNetworkSession())
    .configuration(\.apiVersion, "v2")

Read the value inside a modifier using the @Config property wrapper:

struct APIVersionModifier: RequestModifier {
    @Config(\.apiVersion) var version

    func body(_ content: consuming RequestModifierContent) -> some Request {
        content.path(version)
    }
}

Client Modifiers

Stack behaviour onto any client by chaining modifiers.

Authentication

let client = SessionClient(URLSessionNetworkSession())
    .baseURL(#url("https://api.example.com"))
    .authenticated(by: MyAuthProvider())

Implement AuthenticationProvider for custom schemes:

struct BearerTokenProvider: AuthenticationProvider {
    let token: String

    func authenticate(_ request: PreparedRequest) throws(SwiftNetworkingError) -> PreparedRequest {
        var authenticated = request
        authenticated.headers = (authenticated.headers ?? Headers())
            .authorization(.bearer(token: token))
        return authenticated
    }

    func refresh() async throws(SwiftNetworkingError) {}
}

For OAuth 2.0 with automatic token refresh, use the built-in OAuthAuthenticationProvider. See Authentication.

Retry

let client = SessionClient(URLSessionNetworkSession())
    .retry(RetryPolicy(maxRetries: 3, backoff: .exponential(base: .seconds(1))))

Logging

let client = SessionClient(URLSessionNetworkSession())
    .logging([.request, .response])    // log request/response lines
    .logging(.all)                      // log everything including bodies
    .logging(.all, redacting: [.authorization]) // redact sensitive headers

Waiting for connectivity

let client = SessionClient(URLSessionNetworkSession())
    .waitingForConnectivity(ConnectionMonitor())

Testing

SwiftNetworkingTesting provides MockNetworkSession — a drop-in replacement for URLSessionNetworkSession that records every request, returns pre-configured stub responses, and integrates with swift-testing for built-in assertion helpers.

import SwiftNetworkingTesting

let session = MockNetworkSession()
let client = SessionClient(session)

session.stub(.method(.get) && .host("api.example.com"), .json(User(id: 1, name: "Alice")))

let user = try await client.send(GetUser(id: 1)).content
session.expectRequest(.method(.get))

See Testing for the full guide including stubs, matchers, assertions, WebSocket and SSE testing, and strict mode.


Advanced Topics

Topic Description
Authentication OAuth 2.0, custom auth providers, per-request auth
WebSocket Full-duplex WebSocket connections with typed messages
Server-Sent Events SSE streams with automatic parsing
Resumable Uploads Chunked multi-part uploads with resume support
Security Certificate pinning, Keychain storage
Connection Monitoring Network reachability observation
Testing MockNetworkSession, stubs, matchers, and assertions

License

SwiftNetworking is available under the Apache 2.0 license. See the LICENSE file for details.

Description

  • Swift Tools 6.3.0
View More Packages from this Author

Dependencies

Last updated: Mon Jun 22 2026 00:09:42 GMT-0900 (Hawaii-Aleutian Daylight Time)