Skip to content

✨ A fully declarative Swift networking library inspired by SwiftUI and macros. Build expressive, composable, and testable network requests with ease.

License

Notifications You must be signed in to change notification settings

SwiftyJoeyy/swift-networking

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

86 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GitHub Release License Swift

Swift Networking

Swift Networking is a modern Swift networking library built entirely around a declarative programming model. From defining requests to configuring clients and handling responses, everything is expressed clearly and fluentlyy.

Inspired by Swift & SwiftUI’s design philosophy, it allows you to define network behavior in a way that is readable, modular, and test-friendly — all while keeping boilerplate to a minimum.

✨ Highlights

  • 🧾 Fully declarative request & response design
  • ⚙️ Custom clients, interceptors, headers, and parameters via DSL
  • 🔄 Built-in support for request/response modifiers and interceptors
  • 🧪 Easy-to-test and modular architecture
  • 🧰 Modular, extensible architecture

🚀 Getting Started

This guide walks you through the core building blocks:

  1. Defining a request.
  2. Adding headers, parameters, and body.
  3. Building a client.
  4. Sending a request.

1. Defining a request

To define a request, use the @Request macro. This adds conformance to the Request protocol, which requires a request property similar to SwiftUI’s body. Inside that property, you start with an HTTPRequest, which represents the core of the request.

You can:

  • Provide a url to override the client’s base URL
  • Add an optional path
  • Set the HTTP method using method(_:)
  • Append extra path components using appending(_:) Example
@Request struct TestingRequest {
    var request: some Request {
        HTTPRequest(url: "https://www.example.com", path: "gallery")
            .method(.get)
            .appending("cats", "images")
    }
}

This creates a simple GET request to https://www.google.com/test/cats/images.

You can also compose and override requests:

@Request struct TestRequest {
    @Header var test: String {
        return "Computed"
    }
    var request: some Request {
        TestingRequest() // TestingRequest instead of HTTPRequest
            .timeout(90)
            .method(.post)
    }
}

2. Adding Headers, Parameters, and Body

Networking offers multiple ways to add headers, query parameters, and request bodies. You can use macros for top level values or chaining modifiers for inline customization.

🧩 Adding Parameters

Use Parameter to create parameters from String, Int, Double, or Bool values, including arrays of those types. You can also use ParametersGroup to group multiple parameters into one modifier.

To add parameters to a request, you use the @Parameter macro, or dynamically through modifier methods:

@Request struct SearchRequest {
    @Parameter var query: String // Using the name of the property
    @Parameter("search_query") var query: String // Using a custom name.
    let includeFilter: Bool
    var request: some Request {
        HTTPRequest(path: "search")
            .appendingParameters {
                Parameter("query", value: "cats")
                if includeFilter {
                    Parameter("filter", value: "popular")
                }
            }.appendingParameter("sorting", value: "ascending")
            .appendingParameter("filters", values: ["new", "free"])
    }
}

📬 Adding Headers

Use Header to define headers from String, Int, Double, or Bool values. You can also use HeadersGroup if you want to group multiple headers in a single modifier, or inject a raw dictionary [String: String]. You also get convenience types like: AcceptLanguage, ContentDisposition & ContentType. Similar to parameters, headers can also be declared statically using the @Header macro or applied dynamically using modifiers:

@Request struct AuthenticatedRequest {
    @Header var token: String // Using the name of the property.
    @Header("Authorization") var token: String // Using a custom name.
    let includeLang: Bool
    var request: some Request {
        HTTPRequest(path: "me")
            .additionalHeaders {
                Header("Custom-Header", value: "value")
                if includeLang {
                    AcceptLanguage("en")
                }
            }.additionalHeader("version", value: "1.0")
    }
}

📦 Adding a Request Body

Currently, Networking supports JSON and FormData request bodies. To apply a body to a request, you can use the body(_:) modifier, or convenience modifiers for json:

✅ Using .body(...) Modifier
HTTPRequest(path: "upload")
    .body {
        // JSON
        JSON(["dict": "data"]) // Dictionary
        JSON(Data()) // Raw data.
        JSON(EncodableUser()) // Encodable types.

        // FormData
        FormData {
            FormDataBody( /// Raw data
                "Image",
                data: Data(),
                fileName: "image.png",
                 mimeType: .png
            )
            FormDataFile( // Data from a file.
                "File",
                fileURL: URL(filePath: "filePath"),
                fileName: "file",
                 mimeType: .fileURL
            )
        }
    }
✅ Using JSON Convenience Modifiers
HTTPRequest(path: "create")
    .json(["name": "John", "age": 30]) // Dictionary.
    .json(Data()) // Raw data.
    .json(EncodableUser()) // Encodable types.

Caution

If multiple .body() or .json() modifiers are used, the last one overrides the previous. This applies to all modifiers, modifier order matters.

🧱 Inline Modifiers with HTTPRequest

You can also include modifiers directly in the HTTPRequest initializer. This gives you a clean, SwiftUI style declaration for most common request scenarios.

HTTPRequest(path: "submit") {
    Header("X-Token", value: "123")
    Parameter("query", value: "swift")
    JSON(["key": "value"])
}

3. Building a client

To define a networking client, use the @Client macro. This macro adds conformance to the NetworkClient protocol, which requires a session property. This property returns a Session instance that describes how your requests should be configured and executed.

Important

Do not call the session property directly. It’s a computed property that creates a new session each time it’s accessed. Always send requests using the client instance itself (e.g. try await client.dataTask(...)). You should also hold on to the client instance created either using a singleton or by dependency injection to avoid creating multiple instances.

Inside the session property, you can create and customize a Session using a closure that returns a configured URLSessionConfiguration. From there, you can apply additional behaviors such as Base URL, Logging, Retry policy, Authorization, Response validation, Encoding/decoding strategies.

@Client struct MyClient {
    var session: Session {
        Session {
            URLSessionConfiguration.default
                .urlCache(.shared)
                .requestCachePolicy(.returnCacheDataElseLoad)
                .timeoutIntervalForRequest(90)
                .timeoutIntervalForResource(90)
                .httpMaximumConnectionsPerHost(2)
                .waitForConnectivity(true)
                .headers {
                    Header("Key", value: "Value")
                }
        }
        .authorization(BearerAuthProvider())
        .baseURL("https://example.com")
        .decode(with: JSONDecoder())
        .encode(with: JSONEncoder())
        .retry(limit: 2, delay: 2)
        .enableLogs(true)
        .validate(for: [.badGateway, .created])
    }
}

⚙️ Custom Configuration Values

You can define custom configuration keys using the @Config macro & extending ConfigurationValues:

extension ConfigurationValues {
    @Config var customConfig = CustomValue()
}

Then, you can set it on a task or on the session using the modifier configuration(_:_:):

@Client struct MyClient {
    var session: Session {
        Session()
            .configuration(\.customConfig, CustomValue())
    }
}

let data = try await client.dataTask(TestingRequest())
    .configuration(\.customConfig, CustomValue())
    .response()

This allows you to define project-specific config values and inject them anywhere in your request or session pipeline.

4. Sending a request

To send a request, you start by creating a task from a client. The framework provides two main types of tasks:

Each task can be configured individually using the same modifiers available on Session (e.g. retry, decoding, etc.), giving you full control per request.

let task = MyClient()
    .dataTask(MyRequest())
    .retry(limit: 2)
    .validate(for: [.ok, .notModified])

To start a task you either call resume() manually, or access the response directly (recommended) using response() or decode it to a specific type using decode(as:)

let task = MyClient().dataTask(MyRequest())

task.resume()
let result = try await task.response()
let user = try await task.decode(as: User.self)

Note

A task will only send the request once. If response() or decode(as:) is called multiple times, the framework will await the result of the first call instead of resending the request.

📦 Installation

Add via Swift Package Manager:

.package(url: "https://github.com/SwiftyJoeyy/swift-networking.git", from: "1.0.0")

Then add "Networking" to your target dependencies.

🛣️ Planned Features

These enhancements are planned for future releases of Networking to further improve flexibility, control, and developer experience:

  • 🪄 Simplified Request API Quick request execution using a default client instance for lightweight use cases.

  • 🔄 Resumable Downloads Support for partial downloads and automatic resume handling across app launches or interruptions.

  • 📤 Upload Task Support Upload data or files with progress tracking, cancellation, and retry support.

  • 🏷️ Request Tagging Tag and categorize requests into logical groups for analytics, cancellation, debugging, and tracing.

  • 🧪 Built-in Testing Support Tools for mocking, stubbing, and asserting requests and responses with zero boilerplate.

  • 🔄 Request Execution & Prioritization Control request flow with custom executors, in-flight limits, and per-task priority that can escalate or drop dynamically.

  • 📽 Request Recording & Playback Capture and replay real request traffic for debugging, offline development, and test validation.

📖 Documentation

The documentation is provided by swiftpackageindex.

📄 License

Licensed under the Apache 2.0 License. See the LICENSE file.

About

✨ A fully declarative Swift networking library inspired by SwiftUI and macros. Build expressive, composable, and testable network requests with ease.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages