Networking brings together URLSession, async-await (or Combine ), Decodable and Generics to simplify connecting to a JSON api.
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"])Alex from Rebeloper made a fantastic video tutorial, check it out here!
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.
- Build a concise Api
 - Automatically map your models
 - Uses latest Apple's Combine / asyn-await
 -  Compatible with native 
Decodableand any JSON Parser - Embarks a built-in network logger
 - Pure Swift, simple, lightweight & 0 dependencies
 
- Install it
 - Create a Client
 - Make your first call
 - Get the type you want back
 - Pass params
 - Upload multipart data
 - Add Headers
 - Add Timeout
 - Cancel a request
 - Log Network calls
 - Handling errors
 - Support JSON-to-Model parsing
 - Design a clean api
 
Networking is installed via the official Swift Package Manager.
Select Xcode>File> Swift Packages>Add Package Dependency...
 and add https://github.com/freshOS/Networking.
let client = NetworkingClient(baseURL: "https://jsonplaceholder.typicode.com")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)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("")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 = .jsonFor 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)Headers are added via the headers property on the client.
client.headers["Authorization"] = "[mytoken]"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 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()3 log levels are supported: off, info, debug
client.logLevel = .debugErrors 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)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.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") } }