EZNetworking

4.2.0

A lightweight Swift networking library for handling API requests.
Aldo10012/EZNetworking

What's New

4.2.0

2025-10-25T03:11:07Z

What's new?

Adding support for Multi-Part Form Data
Also some refactor in HTTPBody typealias and reorganize some files

What is Multi-part Form?

You send multiple β€œparts” in one request body, each separated by a boundary.
Each part has its own headers, name, and content, so you can send several things at once.

Example:

POST /upload
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary12345

------WebKitFormBoundary12345
Content-Disposition: form-data; name="username"

Daniel
------WebKitFormBoundary12345
Content-Disposition: form-data; name="profilePicture"; filename="me.jpg"
Content-Type: image/jpeg

<binary image data>
------WebKitFormBoundary12345--

So instead of one blob, you’re uploading a mini HTTP message for each field.

πŸ’‘ When do you need multipart/form-data?

  • You’re submitting a form with multiple fields or files
    • For example, uploading both user info + profile picture in a single request.
    • Common in web forms and REST APIs for file uploads.
  • The API expects form data
    • Many APIs (especially older REST ones) require this format for file uploads.
    • Example: AWS S3 pre-signed POST, Google Drive, Imgur, etc.
  • You need per-part metadata
    • Each part can have its own Content-Type, filename, etc.
    • You can mix text, JSON, and binary files in one upload.

Example usage

let parts: [MultipartFormPart] = [
    MultipartFormPart.fieldPart(
        name: "username",
        value: "Daniel"
    ),
    MultipartFormPart.filePart(
        name: "profile_picture",
        data: fileData,
        filename: "profile.jpg",
        mimeType: .jpeg
    ),
    MultipartFormPart.dataPart(
        name: "metadata",
        data: Data(encodable: user)!,
        mimeType: .json
    )
]
let multippartFormData = MultipartFormData(parts: parts, boundary: "SOME_BOUNDARY")

// example usage on Request

let request = RequestFactoryImpl().build(
    httpMethod: .POST,
    baseUrlString: "https://www.example.com/upload",
    parameters: nil,
    headers: [
        .contentType(.multipartFormData(boundary: "SOME_BOUNDARY"))
    ],
    body: nil 
    // dont inject multippartFormData into the request body. Inject it into DataUploader instead.
    // Reason for this is DataUploader internally uses `URLSession.shared.uploadTask()` which is optimized for uploading data to a server. It takes `data` as an argument and ignores the data provided in `URLRequest.httpBody` 
)

// use DataUploader for uploading the data to a server

for await event in DataUploader().uploadDataStream(multippartFormData.toData()!, with: request) {
  switch event {
  case .progress(let value): // handle progress
  case .success(let data): // handle success
  case .failure(let error): // handle error
  }
}

The above will result in a HTTPBody data looking like this:

"""
--SOME_BOUNDARY
Content-Disposition: form-data; name="username"
Content-Type: text/plain

Daniel
--SOME_BOUNDARY
Content-Disposition: form-data; name="profile_picture"; filename="profile.jpg"
Content-Type: image/jpeg

<binary image data>
--SOME_BOUNDARY
Content-Disposition: form-data; name="metadata"
Content-Type: application/json

{"username":"Daniel","email":"daniel@gmail.com"}
--SOME_BOUNDARY--

"""

EZNetworking

Swift Platform SPM Compatible

EZNetworking is a powerful, lightweight Swift networking library that simplifies API interactions in your iOS applications. Built with modern Swift features, it provides an intuitive interface for making HTTP requests, handling responses, and managing network operations.

Key Features πŸš€

  • Modern Swift Support: Built with Swift 5.9 and iOS 15.0+
  • Async/Await Integration: First-class support for Swift concurrency
  • Type-Safe Networking: Strong typing for requests and responses
  • Flexible Request Building: Multiple approaches to creating requests
  • Comprehensive Interceptors: Full request/response pipeline control
  • Built-in Caching: Efficient response caching system
  • File & Image Downloads: Easy-to-use download utilities
  • Extensive Testing: 100% unit test coverage

Table of Contents πŸ“‘

Installation πŸ“¦

Swift Package Manager

Add EZNetworking to your project using Swift Package Manager:

dependencies: [
    .package(url: "https://github.com/Aldo10012/EZNetworking.git", from: "4.1.0")
]

Or through Xcode:

  1. Go to File > Add Packages
  2. Enter: https://github.com/Aldo10012/EZNetworking.git
  3. Select version: 3.1.0 or later

Quick Start Guide πŸš€

Here's a simple example to get you started:

// Create a request
let request = RequestFactoryImpl().build(
    httpMethod: .GET,
    urlString: "https://api.example.com/data",
    parameters: [.init(key: "userId", value: "123")]
)

// Using async/await
do {
    let response = try await AsyncRequestPerformer().perform(
        request: request,
        decodeTo: UserData.self
    )
    print("User data: \(response)")
} catch {
    print("Error: \(error)")
}

Building Requests πŸ—οΈ

EZNetworking provides three ways to create requests:

  1. Using RequestFactory for quick, one-line requests
  2. Using RequestBuilder for step-by-step request construction
  3. Implementing the Request protocol for reusable API endpoints

Using RequestFactory

Perfect for quick, one-line request creation:

let request = RequestFactoryImpl().build(
    httpMethod: .POST,
    urlString: "https://api.example.com/users",
    parameters: [
        .init(key: "name", value: "John Doe"),
        .init(key: "email", value: "john@example.com")
    ],
    headers: [
        .accept(.json),
        .contentType(.json)
    ],
    body: .jsonString("{\"role\":\"user\"}"),
    timeoutInterval: 30,
    cachePolicy: .useProtocolCachePolicy
)

Using RequestBuilder

Ideal for complex requests with multiple configurations:

let request = RequestBuilderImpl()
    .setHttpMethod(.POST)
    .setBaseUrl("https://api.example.com")
    .setParameters([
        .init(key: "api_version", value: "v2")
    ])
    .setHeaders([
        .accept(.json),
        .authorization(.bearer("YOUR_TOKEN"))
    ])
    .setBody(.jsonString("{\"data\":\"value\"}"))
    .setTimeoutInterval(30)
    .setCachePolicy(.useProtocolCachePolicy)
    .build()

Request Protocol

The Request protocol allows you to create reusable request definitions:

struct UserRequest: Request {
    let userId: String
    
    var httpMethod: HTTPMethod { .GET }
    var baseUrlString: String { "https://api.example.com" }
    var parameters: [HTTPParameter]? {[
        .init(key: "user_id", value: userId),
        .init(key: "version", value: "v2")
    ]}
    var headers: [HTTPHeader]? {[
        .accept(.json),
        .contentType(.json),
        .authorization(.bearer("YOUR_TOKEN"))
    ]}
    var body: HTTPBody? { nil }
    var timeoutInterval: TimeInterval { 30 }
    var cachePolicy: URLRequest.CachePolicy { .useProtocolCachePolicy }
}

// Usage
let userRequest = UserRequest(userId: "123")
let response = try await AsyncRequestPerformer().perform(
    request: userRequest,
    decodeTo: UserData.self
)

Request Components πŸ”§

HTTP Methods

Supported HTTP methods:

public enum HTTPMethod: String {
    case GET, POST, PUT, DELETE
}

Query Parameters

Add query parameters to your requests:

let parameters: [HTTPParameter] = [
    .init(key: "page", value: "1"),
    .init(key: "limit", value: "20"),
    .init(key: "sort", value: "desc")
]

// With RequestFactory
let request1 = RequestFactoryImpl().build(
    httpMethod: .GET,
    urlString: "https://api.example.com",
    parameters: parameters
)

// With RequestBuilder
let request2 = RequestBuilderImpl()
    .setHttpMethod(.GET)
    .setBaseUrl("https://api.example.com")
    .setParameters(parameters)
    .build()

Headers

EZNetworking provides a type-safe way to add headers:

let headers: [HTTPHeader] = [
    .accept(.json),
    .contentType(.json),
    .authorization(.bearer("YOUR_TOKEN")),
    .custom(key: "X-Custom-Header", value: "custom-value")
]

// Common header types
public enum HTTPHeader {
    case accept(ContentType)
    case contentType(ContentType)
    case authorization(AuthorizationType)
    case custom(key: String, value: String)
    // ... other http header types
}

public enum ContentType: String {
    case json = "application/json"
    case xml = "application/xml"
    case formUrlEncoded = "application/x-www-form-urlencoded"
    case multipartFormData = "multipart/form-data"
    // ... other content types
}

Authorization

Multiple authorization methods are supported:

// Bearer token
.authorization(.bearer("YOUR_TOKEN"))

// Custom auth
.authorization(.custom("Custom-Auth-Value"))

Request Body

Multiple body types are supported:

// JSON String
let jsonBody = HTTPBody.jsonString("{\"key\":\"value\"}")

// Data
let dataBody = HTTPBody.data(someData)

// Form URL Encoded
let formBody = HTTPBody.formUrlEncoded([
    "key1": "value1",
    "key2": "value2"
])

// Multipart Form Data
let multipartBody = HTTPBody.multipartFormData([
    .init(name: "file", fileName: "image.jpg", data: imageData),
    .init(name: "description", value: "Profile picture")
])

Timeout and Cache

Configure request timeout and caching behavior:

// With RequestFactory
let request1 = RequestFactoryImpl().build(
    httpMethod: .GET,
    urlString: "https://api.example.com",
    timeoutInterval: 30,
    cachePolicy: .returnCacheDataElseLoad
)

// With RequestBuilder
let request2 = RequestBuilderImpl()
    .setHttpMethod(.GET)
    .setBaseUrl("https://api.example.com")
    .setTimeoutInterval(30)
    .setCachePolicy(.returnCacheDataElseLoad)
    .build()

Making Network Calls 🌐

Async/Await Usage

Modern Swift concurrency support:

// With response decoding
do {
    let userData = try await RequestPerformer().perform(request: request, decodeTo: UserData.self)
    // Handle decoded response
} catch {
    // Handle error
}

// Without decoding
do {
    try await RequestPerformer().perform(request: request, decodeTo: EmptyResponse.self)
    // Handle success
} catch {
    // Handle error
}

Completion Handlers

Traditional callback-based approach:

// With response decoding
RequestPerformer().performTask(request: request, decodeTo: UserData.self) { result in
    switch result {
    case .success(let userData):
        // Handle decoded response
    case .failure(let error):
        // Handle error
    }
}

// Without decoding
RequestPerformer().performTask(request: request, decodeTo: EmptyResponse.self) { result in
    switch result {
    case .success:
        // Handle success
    case .failure(let error):
        // Handle error
    }
}

Task Control

Control over URLSessionTask:

// Store task reference
let task = RequestPerformer().performTask(request: request) { _ in
    // Handle completion
}

// Cancel task if needed
task.cancel()

// Resume suspended task
task.resume()

// Suspend task
task.suspend()

// Get task state
print(task.state) // running, suspended, canceling, completed

Publishers

If you prefer using the Combine framework

let cancellables = Set<AnyCancellable>()

RequestPerformer()
    .performPublisher(request: CustomRequest(), decodeTo: CustomeType.swift)
    .sink(receiveCompletion: { completion in
        // handle completion
    }, receiveValue: { customType in
        // handle response
    })
    .store(in: &cancellables)

Download Features πŸ“₯

File Downloads

let fileURL = URL(string: "https://example.com/file.pdf")!

// Async/await
do {
    let localURL = try await FileDownloader().downloadFile(with: fileURL)
    // Handle downloaded file
} catch {
    // Handle error
}

// Completion handler with progress tracking
let task = FileDownloader().downloadFile(url: testURL) { result in
    switch result {
    case .success:
        // handle the returned local URL path. Perhaps write and save it in FileManager
    case .failure(let error):
        // handle error
    }
}

// Cancel download if needed
task.cancel()

// Combine Publishers
let cancellables = Set<AnyCancellable>()
FileDownloader()
    .downloadFilePublisher(url: URL, progress: {
        // handle progress
    })
    .sink(receiveCompletion: { completion in
        // handle completion
    }, receiveValue: { localURL in
        // handle response
    })
    .store(in: &cancellables)

Upload Features πŸ“₯

Uploading raw data

Async Await

do {
  let resultData = try await DataUploader().uploadData(data, with: request, progress: { progress in
    // track progress
  })
  // handle success
} catch {
  // handle error
}

AsyncStream

for await event in DataUploader().uploadDataStream(data, with: request) {
  switch event {
  case .progress(let value): // handle progress
  case .success(let data): // handle success
  case .failure(let error): // handle error
  }
}

Completion Handler

DataUploader().uploadData(data, with: request, progress: { progress in 
  // track progress
}, completion: { result in
  switch result {
  case: .success(let data):
    // handle success
  case: .failure(let error):
    // handle error
  }
})

Combine Publisher

DataUploader().uploadDataPublisher(data, with: request: progress: { progress in
  // track progress
})
.sink { completion in
  switch completion {
  case .failure: // handle error
  case .finished: // handle completion
  }
} receiveValue: { data in
  // handle data
}
.store(in: &cancellables)

Uploading File

To get a file that exists in your bundle, do this

fileURL = Bundle.main.url(forResource: "myDocument", withExtension: "txt")

To get a file that exists in your files directory, do this

let customFileURL = URL(fileURLWithPath: "/Users/username/Documents/myFile.pdf")

Async Await

do {
  let resultData = try await FileUploader().uploadFile(fileURL, with: request, progress: { progress in
    // track progress
  })
  // handle success
} catch {
  // handle error
}

AsyncStream

for await event in FileUploader().uploadFileStream(fileURL, with: request) {
  switch event {
  case .progress(let value): // handle progress
  case .success(let data): // handle success
  case .failure(let error): // handle error
  }
}

Completion Handler

FileUploader().uploadFileTask(fileURL, with: request, progress: { progress in 
  // track progress
}, completion: { result in
  switch result {
  case: .success(let data):
    // handle success
  case: .failure(let error):
    // handle error
  }
})

Combine Publisher

FileUploader().uploadFilePublisher(fileURL, with: request: progress: { progress in
  // track progress
})
.sink { completion in
  switch completion {
  case .failure: // handle error
  case .finished: // handle completion
  }
} receiveValue: { data in
  // handle data
}
.store(in: &cancellables)

Uploading multipart form data

let parts: [MultipartFormPart] = [
    MultipartFormPart.fieldPart(
        name: "username",
        value: "Daniel"
    ),
    MultipartFormPart.filePart(
        name: "profile_picture",
        data: fileData,
        filename: "profile.jpg",
        mimeType: .jpeg
    ),
    MultipartFormPart.dataPart(
        name: "metadata",
        data: Data(encodable: user)!,
        mimeType: .json
    )
]
let multippartFormData = MultipartFormData(parts: parts, boundary: "SOME_BOUNDARY")

// example usage on Request

let request = RequestFactoryImpl().build(
    httpMethod: .POST,
    baseUrlString: "https://www.example.com/upload",
    parameters: nil,
    headers: [
        .contentType(.multipartFormData(boundary: "SOME_BOUNDARY"))
    ],
    body: nil 
    // dont inject multippartFormData into the request body. Inject it into DataUploader instead.
    // Reason for this is DataUploader internally uses `URLSession.shared.uploadTask()` which is optimized for uploading data to a server. It takes `data` as an argument and ignores the data provided in `URLRequest.httpBody` 
)

// use DataUploader for uploading the data to a server

for await event in DataUploader().uploadDataStream(multippartFormData.toData()!, with: request) {
  switch event {
  case .progress(let value): // handle progress
  case .success(let data): // handle success
  case .failure(let error): // handle error
  }
}

Advanced Features πŸ”§

Interceptors

EZNetworking provides a comprehensive set of interceptors for customizing network behavior:

Cache Interceptor

Control caching behavior:

class CustomCacheInterceptor: CacheInterceptor {
    func urlSession(
        _ session: URLSession,
        dataTask: URLSessionDataTask,
        willCacheResponse proposedResponse: CachedURLResponse
    ) async -> CachedURLResponse? {
        // Customize caching behavior
        return proposedResponse
    }
}

let delegate = SessionDelegate()
delegate.cacheInterceptor = CustomCacheInterceptor()

Authentication Interceptor

Handle authentication challenges:

class CustomAuthInterceptor: AuthenticationInterceptor {
    func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        didReceive challenge: URLAuthenticationChallenge
    ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
        // Handle authentication
        return (.useCredential, URLCredential(
            user: "username",
            password: "password",
            persistence: .forSession
        ))
    }
}

let delegate = SessionDelegate()
delegate.authenticationInterceptor = CustomAuthInterceptor()

Redirect Interceptor

Control URL redirections:

class CustomRedirectInterceptor: RedirectInterceptor {
    func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        willPerformHTTPRedirection response: HTTPURLResponse,
        newRequest request: URLRequest
    ) async -> URLRequest? {
        // Handle redirection
        return request
    }
}

let delegate = SessionDelegate()
delegate.redirectInterceptor = CustomRedirectInterceptor()

Metrics Interceptor

Collect performance metrics:

class CustomMetricsInterceptor: MetricsInterceptor {
    func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        didFinishCollecting metrics: URLSessionTaskMetrics
    ) {
        // Process metrics
        print("Task duration: \(metrics.taskInterval.duration)")
    }
}

let delegate = SessionDelegate()
delegate.metricsInterceptor = CustomMetricsInterceptor()

Task Lifecycle Interceptor

Monitor task lifecycle events:

class CustomLifecycleInterceptor: TaskLifecycleInterceptor {
    func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        didCompleteWithError error: Error?
    ) {
        // Handle task completion
    }
    
    func urlSession(
        _ session: URLSession,
        taskIsWaitingForConnectivity task: URLSessionTask
    ) {
        // Handle connectivity waiting
    }
    
    func urlSession(
        _ session: URLSession,
        didCreateTask task: URLSessionTask
    ) {
        // Handle task creation
    }
}

let delegate = SessionDelegate()
delegate.taskLifecycleInterceptor = CustomLifecycleInterceptor()

Data Task Interceptor

Process incoming data:

class CustomDataTaskInterceptor: DataTaskInterceptor {
    func urlSession(
        _ session: URLSession,
        dataTask: URLSessionDataTask,
        didReceive data: Data
    ) {
        // Process received data
    }
    
    func urlSession(
        _ session: URLSession,
        dataTask: URLSessionDataTask,
        didReceive response: URLResponse
    ) async -> URLSession.ResponseDisposition {
        // Handle response
        return .allow
    }
}

let delegate = SessionDelegate()
delegate.dataTaskInterceptor = CustomDataTaskInterceptor()

Download Task Interceptor

Monitor download progress:

class CustomDownloadInterceptor: DownloadTaskInterceptor {
    func urlSession(
        _ session: URLSession,
        downloadTask: URLSessionDownloadTask,
        didFinishDownloadingTo location: URL
    ) {
        // Handle download completion
    }
    
    func urlSession(
        _ session: URLSession,
        downloadTask: URLSessionDownloadTask,
        didWriteData bytesWritten: Int64,
        totalBytesWritten: Int64,
        totalBytesExpectedToWrite: Int64
    ) {
        // Track download progress
        let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
        print("Download progress: \(progress)")
    }
}

let delegate = SessionDelegate()
delegate.downloadTaskInterceptor = CustomDownloadInterceptor()

Upload Task Interceptor

Monitor upload progress

class CustomUploadTaskInterceptor: UploadTaskInterceptor {
    var progress: (Double) -> Void

    func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
        // Track upload progress
    }
}

Stream Task Interceptor

Handle streaming operations:

class CustomStreamInterceptor: StreamTaskInterceptor {
    func urlSession(
        _ session: URLSession,
        streamTask: URLSessionStreamTask,
        didBecome inputStream: InputStream,
        outputStream: OutputStream
    ) {
        // Handle streams
    }
    
    func urlSession(
        _ session: URLSession,
        readClosedFor streamTask: URLSessionStreamTask
    ) {
        // Handle read close
    }
}

let delegate = SessionDelegate()
delegate.streamTaskInterceptor = CustomStreamInterceptor()

WebSocket Task Interceptor

Handle WebSocket communications:

class CustomWebSocketInterceptor: WebSocketTaskInterceptor {
    func urlSession(
        _ session: URLSession,
        webSocketTask: URLSessionWebSocketTask,
        didOpenWithProtocol protocol: String?
    ) {
        // Handle WebSocket open
    }
    
    func urlSession(
        _ session: URLSession,
        webSocketTask: URLSessionWebSocketTask,
        didCloseWith closeCode: URLSessionWebSocketTask.CloseCode,
        reason: Data?
    ) {
        // Handle WebSocket close
    }
}

let delegate = SessionDelegate()
delegate.webSocketTaskInterceptor = CustomWebSocketInterceptor()

Session Management

Configure and manage URLSession behavior:

// Create session delegate with interceptors
let delegate = SessionDelegate()
delegate.cacheInterceptor = CustomCacheInterceptor()
delegate.authenticationInterceptor = CustomAuthInterceptor()
delegate.metricsInterceptor = CustomMetricsInterceptor()

// Create performer with custom session delegate. Works for RequestPerformer and AsyncRequestPerformer
let performer = RequestPerformer(sessionDelegate: delegate)

// Use performer for requests
performer.performTask(request: request) { result in
    // Handle result
}

Error Handling 🚨

EZNetworking provides comprehensive error handling:

public enum NetworkingError: Error {
    case internalError(InternalError)     /// any internal error
    case httpError(HTTPError)             /// any HTTP status code error
    case urlError(URLError)               /// any URL error
}

// Error handling example
do {
    let response = try await AsyncRequestPerformer().perform(request: request, decodeTo: UserData.self)
    // do something with the response
} catch let error as NetworkingError {
    switch error {
    case .internalError(let internalError):
        // some internal error, such as failed to decode or the URL is not valid
    case .httpError(let httpError):
        // some HTTP error, such as status code 404
    case .urlError(let uRLError):
        // some error of type URLError
    }
}

Contributing 🀝

Contributions are welcome! If you have an idea to improve EZNetworking, please feel free to submit and open a pull request or open an issue.

License πŸ“„

EZNetworking is available under the MIT license. See the LICENSE file for more info.

Description

  • Swift Tools 5.9.0
View More Packages from this Author

Dependencies

  • None
Last updated: Sun Oct 26 2025 12:31:38 GMT-0900 (Hawaii-Aleutian Daylight Time)