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)
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
- Compatible with native
Codable
and any JSON Parser - Embarks a built-in network logger
- Pure Swift, simple, lightweight & 0 dependencies
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.
Networking is part of freshOS iOS toolset. Try it in an example App ! Download Starter Project
- Install it
- Create a Client
- Make your first call
- Get the type you want back
- Pass params
- Upload multipart data
- Add Headers
- Cancel a request
- Log Network calls
- 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
& delete
methods on the client to make calls.
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), 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("")
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)
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)
Headers are added via the headers
property on the client.
client.headers["Authorization"] = "[mytoken]"
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.logLevels = .debug
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
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")
}
}