/Networking

⚡️ Elegantly connect to a REST JSON Api. URLSession + Combine + Decodable + Generics = <3

Primary LanguageSwiftMIT LicenseMIT

Networking

Networking

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

Networking brings together URLSession, Combine, Decodable and Generics to make connecting to a JSON api a breeze.

struct Api: NetworkingService {

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

    func fetchPost() -> AnyPublisher<Post, Error> {
        get("/posts/1")
    }
}

Later...

let api = Api()
api.fetchPost().sink(receiveCompletion: { _ in }) { post in
    // Get back some post \o/
}.store(in: &cancellables)

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

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

Welcome the future. Bye ws , Hello Networking.

Networking is the next generation of the ws project. The improvements are: Using Combine native Apple's framework over Then Promise Library, removing Arrow dependency to favour Codable (Arrow can still be adapted easily though) and removing the Alamofire dependency in favour of a simpler purely native URLSession implementation.
In essence, less dependencies and more native stuff.

Try it!

Networking is part of freshOS iOS toolset. Try it in an example App ! Download Starter Project

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 & delete methods on the client to make calls.

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), NetworkingJSONDecodable(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.

client.postsPublisher("/posts/1", params: ["optin" : true ])
    .sink(receiveCompletion: { _ in }) { (data:Data) in
      //  response
    }.store(in: &cancellables)

Upload multipart data

For multipart calls (post/put), just pass a MultipartData struct to the multipartData parameter.

let params: [String: CustomStringConvertible] = [ "type_resource_id": 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.logLevels = .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 needs to conform to the NetworkingJSONDecodable protocol.

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.

This default extension is already provided for the native Decodable type. So if your models are Decodable then you just have to add:

extension Mymodel: NetworkingJSONDecodable { }

You can support any JSON parsing by replacing the code above with whatever JSON parsing library you are using \o/ !

// TODO Document network.defaultCollectionParsingKeyPath = "collection" Clean Api

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: Codable {
    let id: String
    let title: String
    let content: String
}

Make your Article NetworkingJSONDecodable, this is a one liner since Codable is supported by default.

extension Article: NetworkingJSONDecodable {}

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) -> 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")
    }
}