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
Decodable
and 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 = .json
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]"
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 = .debug
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)
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")
}
}