freshOS
Networking
Swift

⚡️ Concise networking code leveraging async-await, Decodable & Generics.

Last updated Jun 30, 2026
884
Stars
58
Forks
4
Issues
0
Stars/day
Attention Score
72
Language breakdown
Swift 100.0%
Files click to expand
README

Networking

Networking

Language: Swift 5 Platform: iOS 13+ SPM compatible License: MIT Build Status codebeat badge Release version

Networking brings together URLSession, async-await (or Combine ), Decodable and Generics to simplify connecting to a JSON api.

Demo time 🍿

Networking turns this:

let config = URLSessionConfiguration.default let urlSession = URLSession(configuration: config, delegate: nil, delegateQueue: nil) var urlRequest = URLRequest(url: URL(string: "https://jsonplaceholder.typicode.com/users")!) urlRequest.httpMethod = "POST" urlRequest.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") urlRequest.httpBody = "firstname=Alan&lastname=Turing".data(using: .utf8) let (data, _) = try await urlSession.data(for: urlRequest) let decoder = JSONDecoder() let user = try decoder.decode(User.self, from: data)

into:

let network = NetworkingClient(baseURL: "https://jsonplaceholder.typicode.com") let user: User = try await network.post("/users", params: ["firstname" : "Alan", "lastname" : "Turing"])

Video tutorial

Alex from Rebeloper made a fantastic video tutorial, check it out here!

How

By providing a lightweight client that automates boilerplate code everyone has to write. By exposing a delightfully simple api to get the job done simply, clearly, quickly. Getting swift models from a JSON api is now a problem of the past

URLSession + Combine + Generics + Protocols = Networking.

What

  • [x] Build a concise Api
  • [x] Automatically map your models
  • [x] Uses latest Apple's Combine / asyn-await
  • [x] Compatible with native Decodable and any JSON Parser
  • [x] Embarks a built-in network logger
  • [x] Pure Swift, simple, lightweight & 0 dependencies

Getting Started

Install it

Networking is installed via the official Swift Package Manager.

Select Xcode>File> Swift Packages>Add Package Dependency... and add https://github.com/freshOS/Networking.

Create a Client

let client = NetworkingClient(baseURL: "https://jsonplaceholder.typicode.com")

Make your first call

Use get, post, put, patch & delete methods on the client to make calls.
let data: Data = try await client.get("/posts/1")
All the apis are also available in Combine:
client.get("/posts/1").sink(receiveCompletion: { _ in }) { (data:Data) in
    // data
}.store(in: &cancellables)

Get the type you want back

Networking recognizes the type you want back via type inference. Types supported are Void, Data, Any(JSON), Decodable(Your Model) & NetworkingJSONDecodable

This enables keeping a simple api while supporting many types :

let voidPublisher: AnyPublisher<Void, Error> = client.get("") let dataPublisher: AnyPublisher<Data, Error> = client.get("") let jsonPublisher: AnyPublisher<Any, Error> = client.get("") let postPublisher: AnyPublisher<Post, Error> = client.get("") let postsPublisher: AnyPublisher<[Post], Error> = client.get("")

Pass params

Simply pass a [String: CustomStringConvertible] dictionary to the params parameter.
let response: Data = try await client.posts("/posts/1", params: ["optin" : true ])

Parameters are .urlEncoded by default (Content-Type: application/x-www-form-urlencoded), to encode them as json (Content-Type: application/json), you need to set the client's parameterEncoding to .json as follows:

client.parameterEncoding = .json

Upload multipart data

For multipart calls (post/put), just pass a MultipartData struct to the multipartData parameter.
let params: [String: CustomStringConvertible] = [ "typeresourceid": 1, "title": photo.title]
let multipartData = MultipartData(name: "file",
                                  fileData: photo.data,
                                  fileName: "photo.jpg",
                                   mimeType: "image/jpeg")
client.post("/photos/upload",
            params: params,
            multipartData: multipartData).sink(receiveCompletion: { _ in }) { (data:Data?, progress: Progress) in
                if let data = data {
                    print("upload is complete : \(data)")
                } else {
                    print("progress: \(progress)")
                }
}.store(in: &cancellables)

Add Headers

Headers are added via the headers property on the client.
client.headers["Authorization"] = "[mytoken]"

Add Timeout

Timeout (TimeInterval in seconds) is added via the optional timeout property on the client.
let client = NetworkingClient(baseURL: "https://jsonplaceholder.typicode.com", timeout: 15)

Alternatively,

client.timeout = 15

Cancel a request

Since Networking uses the Combine framework. You just have to cancel the AnyCancellable returned by the sink call.
var cancellable = client.get("/posts/1").sink(receiveCompletion: { _ in }) { (json:Any) in
  print(json)
}
Later ...
cancellable.cancel()

Log Network calls

3 log levels are supported: off, info, debug
client.logLevel = .debug

Handling errors

Errors can be handled on a Publisher, such as:
client.get("/posts/1").sink(receiveCompletion: { completion in
switch completion {
case .finished:
    break
case .failure(let error):
    switch error {
    case let decodingError DecodingError:
        // handle JSON decoding errors
    case let networkingError NetworkingError:
        // handle NetworkingError
        // print(networkingError.status)
        // print(networkingError.code)
    default:
        // handle other error types
        print("\(error.localizedDescription)")
    }
}   
}) { (response: Post) in
    // handle the response
}.store(in: &cancellables)

Support JSON-to-Model parsing.

For a model to be parsable by Networking, it only needs to conform to the Decodable protocol.

If you are using a custom JSON parser, then you'll have to conform to NetworkingJSONDecodable. For example if you are using Arrow for JSON Parsing. Supporting a Post model will look like this:

extension Post: NetworkingJSONDecodable {     static func decode(_ json: Any) throws -> Post {         var t = Post()         if let arrowJSON = JSON(json) {             t.deserialize(arrowJSON)         }         return t     } }

Instead of doing it every models, you can actually do it once for all with a clever extension 🤓.

extension ArrowParsable where Self: NetworkingJSONDecodable {

public static func decode(_ json: Any) throws -> Self { var t: Self = Self() if let arrowJSON = JSON(json) { t.deserialize(arrowJSON) } return t } }

extension User: NetworkingJSONDecodable { } extension Photo: NetworkingJSONDecodable { } extension Video: NetworkingJSONDecodable { } // etc.

Design a clean api

In order to write a concise api, Networking provides the NetworkingService protocol. This will forward your calls to the underlying client so that your only have to write get("/route") instead of network.get("/route"), while this is overkill for tiny apis, it definitely keep things concise when working with massive apis.

Given an Article model

struct Article: DeCodable {     let id: String     let title: String     let content: String }

Here is what a typical CRUD api would look like :

struct CRUDApi: NetworkingService {

var network = NetworkingClient(baseURL: "https://my-api.com")

// Create func create(article a: Article) async throws -> Article { try await post("/articles", params: ["title" : a.title, "content" : a.content]) }

// Read func fetch(article a: Article) async throws -> Article { try await get("/articles/\(a.id)") }

// Update func update(article a: Article) async throws -> Article { try await put("/articles/\(a.id)", params: ["title" : a.title, "content" : a.content]) }

// Delete func delete(article a: Article) async throws { return try await delete("/articles/\(a.id)") }

// List func articles() async throws -> [Article] { try await get("/articles") } }

The Combine equivalent would look like this:

struct CRUDApi: NetworkingService {

var network = NetworkingClient(baseURL: "https://my-api.com")

// Create func create(article a: Article) -> AnyPublisher<Article, Error> { post("/articles", params: ["title" : a.title, "content" : a.content]) }

// Read func fetch(article a: Article) -> AnyPublisher<Article, Error> { get("/articles/\(a.id)") }

// Update func update(article a: Article) -> AnyPublisher<Article, Error> { put("/articles/\(a.id)", params: ["title" : a.title, "content" : a.content]) }

// Delete func delete(article a: Article) -> AnyPublisher<Void, Error> { delete("/articles/\(a.id)") }

// List func articles() -> AnyPublisher<[Article], Error> { get("/articles") } }

© 2026 GitRepoTrend · freshOS/Networking · Updated daily from GitHub