Skip to content

Repository files navigation

Swift GitHub license Twitter

Thin layer for standard network handling, with async/await as a first class citizen.

  • ✅ Vanilla async/await wrappers for CRUD network operations (create / read / update / delete)
  • ✅ async/await download and upload APIs
  • ✅ Type-safe Decodable requests out of the box
  • ✅ Cooperative cancellation: cancelling the surrounding Task cancels the network request
  • ✅ Swift 6 language mode with audited Sendable conformances
  • ✅ 100% URLSession under the hood. Zero third-party dependencies
  • ✅ Fully unit tested

Requirements

  • Swift 6.0+ (Xcode 16+)
  • iOS 17+ / macOS 14+ / tvOS 17+ / watchOS 10+

Installation

Swift Package Manager

In Xcode: File ▸ Add Package Dependencies... and paste

https://github.com/GeekingwithMauri/MauriNet

Or declare it directly in your Package.swift:

dependencies: [
    .package(url: "https://github.com/GeekingwithMauri/MauriNet", branch: "main")
],
targets: [
    .target(name: "YourTarget", dependencies: ["MauriNet"])
]

Usage

CRUD operations with async/await

AsyncRequestManager is the modern entry point of the library. Its CRUD conveniences take any URLRequest and stamp the appropriate HTTP verb on it for you:

import MauriNet

let manager = AsyncRequestManager()
let postsRequest = URLRequest(url: URL(validURL: "https://example.com/api/posts"))

// Create (POST)
let creationReply = try await manager.create(postsRequest, payload: encodedNewPost)

// Read (GET)
let retrievedData = try await manager.read(postsRequest)

// Update (PUT by default, PATCH on demand)
let updateReply = try await manager.update(postsRequest, payload: encodedAmendedPost)
let patchReply = try await manager.update(postsRequest, payload: encodedDelta, method: .patch)

// Delete (DELETE)
try await manager.delete(postsRequest)

Any failure throws a strongly typed NetworkError, mapped from the reply's HTTP status code:

do {
    let retrievedData = try await manager.read(postsRequest)
} catch let error as NetworkError {
    switch error {
    case .notFound:
        // 404: handle the missing resource
    case .unauthorized:
        // 401: refresh credentials
    default:
        print(error.localizedDescription)
    }
}

Decodable requests

Skip the manual decoding dance. Declare your model as the return type and let the manager handle the rest:

struct Post: Decodable {
    let id: Int
    let title: String
}

let posts: [Post] = try await manager.request(postsRequest)

A custom-configured JSONDecoder can be passed as a second argument when needed:

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

let posts: [Post] = try await manager.request(postsRequest, decoder: decoder)

Downloads

let resourceRequest = URLRequest(url: URL(validURL: "https://example.com/resource.zip"))
let localFile = try await manager.download(resourceRequest)
// localFile points to the app's temporary directory: move it to a
// permanent location (or consume it) at your earliest convenience

The destination can also be made explicit by providing an existing directory:

let downloadsFolder = URL.documentsDirectory.appending(path: "Downloads")
let localFile = try await manager.download(resourceRequest, into: downloadsFolder)

Uploads

var uploadRequest = URLRequest(url: URL(validURL: "https://example.com/avatar"))
uploadRequest.httpMethod = HTTPMethod.post.value

// the data to be uploaded
let avatarData = try Data(contentsOf: avatarFileURL)

let serverReply = try await manager.upload(uploadRequest, from: avatarData)

Testability

AsyncRequestManager is backed by the URLSessionable protocol (which URLSession already conforms to). Inject your own conforming double to unit test your networking layer without hitting the actual network:

struct StubbedSession: URLSessionable {
    func retrieveData(for request: URLRequest) async throws -> (Data, URLResponse) {
        // return canned data + response
    }

    func downloadFile(for request: URLRequest) async throws -> (URL, URLResponse) {
        // return canned file location + response
    }

    func upload(_ payload: Data, for request: URLRequest) async throws -> (Data, URLResponse) {
        // return canned server reply + response
    }
}

let testableManager = AsyncRequestManager(session: StubbedSession())

Documentation 🔎📜

All public signatures are fully documented. Reference documentation was generated with Jazzy as explained in this article and can be found here

About

Thin layer for standard network handling

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages