TermiNetwork
TermiNetwork is a networking abstraction layer written on top of Swift's URLRequest that supports multi-environment configuration, routing and automatic model deserialization with Codable.
Features
- [x] Receive responses with the wanted data type (supported: Codable, JSON, UIImage, Data, String)
- [x] Automatic deserialization with Codable
- [x] SwiftyJSON support
- [x] Multi-environment configuration
- [x] Routing
- [x] Error handling
Installation
TermiNetwork is available via CocoaPods. Simply add the following lines to your Podfile and run pod install from your terminal:
platform :ios, '9.0'
use_frameworks!
target 'YourTarget' do
pod 'TermiNetwork', '~> 0.3'
end
Usage
Simple usage
Imagine that you have the following Codable model
struct Todo: Codable {
let id: Int
let title: String
}
You can call the TNRequest(...).start(...) to add a new Todo with the title "Go shopping." like this:
let params = ["title": "Go shopping."]
let headers = ["x-auth": "abcdef1234"]
TNRequest(method: .post, url: "https://myweb.com/api/todos", headers: headers, params: params).start(responseType: Todo.self, onSuccess: { todo in
print(todo)
}) { (error, data) in
print(error)
}
If the request completes successfully (2xx), a new instance of Todo is being created and passed in onSuccess callback. If the deserialization fails for any reason, the onFailure callback is being called with the appropriate error. See more information about errors in Error Handling section.
The JSON response of the service is the following:
{
"id": 5,
"title": "Go shopping."
}
Parameters
method: one of the following supported HTTP methods
.get, .head, .post, .put, .delete, .connect, .options, .trace or .patch
responseType: one of the following supported response types
JSON.self, Codable.self, UIImage.self, Data.self or String.self
onSuccess: a callback returning an object with the data type specified by
onFailure: a callback returning an error+data on failure. There are two cases when this callback being called: the first is that the http status code is different than 2xx and the second is that there is an error with data conversion, e.g. it fails on deserialization of the responseType.
Advanced usage with configuration and custom queue
let myQueue = TNQueue(failureMode: .continue)
myQueue.maxConcurrentOperationCount = 2
let configuration = TNRequestConfiguration(
cachePolicy: .useProtocolCachePolicy,
timeoutInterval: 30,
requestBodyType: .JSON
)
let params = ["title": "Go shopping."]
let headers = ["x-auth": "abcdef1234"]
TNRequest(method: .post,
url: "https://myweb.com/todos",
headers: headers,
params: params,
configuration: configuration).start(queue: myQueue, responseType: JSON.self, onSuccess: { json in
print(json)
}) { (error, data) in
print(error)
}
The request above uses a custom queue myQueue with a failure mode of value .continue (default), which means that the queue continues its execution even if a request fails, and also sets the maximum concurrent operation count to 2. Finally, it uses a TNRequestConfiguration object to provide some additional settings.
Additional parameters
configuration (optional): The configuration object to use. The available configuration properties are:
- cachePolicy: The NSURLRequest.CachePolicy used by NSURLRequest internally. Default value: .useProtocolCachePolicy (see apple docs for available values)
- timeoutInterval: The timeout interval used by NSURLRequest internally. Default value: 60 (see apple docs for more info)
- requestBodyType: It specifies the content type of request params. Available values:
- .xWWWFormURLEncoded (default): The params are being sent with 'application/x-www-form-urlencoded' content type.
- .JSON: The params are being sent with 'application/json' content type.
queue (optional): Specify the queue in which the request will be added. If you omit this argument, the request will be added to a shared queue (TNQueue.shared).
Router
You can organize your requests by creating an Environment (class) and a Router (enum) that conform TNEnvironmentProtocol and TNRouterProtocol respectively. To do so, create an environment enum and at least one router class as shown bellow:
Environment.swift
enum Environment: TNEnvironmentProtocol {
case localhost
case dev
case production
func configure() -> TNEnvironment {
let requestConfiguration = TNRequestConfiguration(cachePolicy: .useProtocolCachePolicy,
timeoutInterval: 30,
requestBodyType: .JSON)
switch self {
case .localhost:
return TNEnvironment(scheme: .https,
host: "localhost",
port: 8080,
requestConfiguration: requestConfiguration)
case .dev:
return TNEnvironment(scheme: .https,
host: "mydevserver.com",
suffix: path("v1"),
requestConfiguration: requestConfiguration)
case .production:
return TNEnvironment(scheme: .http,
host: "myprodserver.com",
suffix: path("v1"),
requestConfiguration: requestConfiguration)
}
}
}
You can optionally pass a requestConfiguration object to make all requests inherit the specified settings. (see 'Advanced usage with configuration and custom queue' above for how to create a configuration object.)
TodosRouter.swift
enum TodosRouter: TNRouterProtocol {
// Define your routes
case list
case show(id: Int)
case add(title: String)
case remove(id: Int)
case setCompleted(id: Int, completed: Bool)
// Set method, path, params, headers for each route
func configure() -> TNRouteConfiguration {
let headers = ["x-auth": "abcdef1234"]
let configuration = TNRequestConfiguration(requestBodyType: .JSON)
switch self {
case .list:
return TNRouteConfiguration(method: .get, path: path("todos"), headers: headers, requestConfiguration: configuration) // GET /todos
case .show(let id):
return TNRouteConfiguration(method: .get, path: path("todo", String(id)), headers: headers, requestConfiguration: configuration) // GET /todos/[id]
case .add(let title):
return TNRouteConfiguration(method: .post, path: path("todos"), params: ["title": title], headers: headers, requestConfiguration: configuration) // POST /todos
case .remove(let id):
return TNRouteConfiguration(method: .delete, path: path("todo", String(id)), headers: headers, requestConfiguration: configuration) // DELETE /todo/[id]
case .setCompleted(let id, let completed):
return TNRouteConfiguration(method: .patch, path: path("todo", String(id)), params: ["completed": completed], headers: headers, requestConfiguration: configuration) // PATCH /todo/[id]
}
}
}
You can optionally pass a requestConfiguration object to specify settings for each route. (see 'Advanced usage with configuration and custom queue' above for instructions of how to create a configuration object.)
Finally use the TNRouter to start a request
TNRouter.start(TodosRouter.add(title: "Go shopping!"), responseType: Todo.self, onSuccess: { todo in
// do something with todo
}) { (error, data) in
// show error
}
TNQueue Hooks
Hooks run before and/or after a request execution in a queue. The following hooks are executed in the default queue:
TNQueue.shared.beforeAllRequestsCallback = {
// e.g. show progress loader
}
TNQueue.shared.afterAllRequestsCallback = { completedWithError in // Bool
// e.g. hide progress loader
}
TNQueue.shared.beforeEachRequestCallback = { request in // TNRequest
// e.g. print log
}
TNQueue.shared.afterEachRequestCallback = { request, data, urlResponse, error in // request: TNRequest, data: Data, urlResponse: URLResponse, error: Error
// e.g. print log
}
Error Handling
Available error cases (TNError) passed in onFailure callback of a TNRequest:
- .environmentNotSet: You didn't set the Environment.
- .invalidURL: The url cannot be parsed, e.g. it contains invalid characters.
- .responseDataIsEmpty: the server response body is empty. You can avoid this error by setting TNRequest.allowEmptyResponseBody to true.
- .responseInvalidImageData: failed to convert response Data to UIImage.
- .cannotDeserialize(Error): e.g. your model's structure and types doesn't match with the server's response. It carries the the error thrown by deserializer (DecodingError.dataCorrupted),
- .cannotConvertToJSON: cannot convert the response Data to JSON object (SwiftyJSON).
- .networkError(Error): e.g. timeout error. It carries the error from URLSessionDataTask.
- .notSuccess(Int): The http status code is different from 2xx. It carries the actual status code of the completed request.
- .cancelled(Error): The request is cancelled. It carries the error from URLSessionDataTask.
In any case you can use the error.localizedDescription method to get a readable error message in onFailure callback.
Example
TNRequest(method: .get, url: "https://myweb.com/todos").start(responseType: JSON.self, onSuccess: { json in
print(json)
}) { (error, data) in
switch error {
case .notSuccess(let statusCode):
debugPrint("Status code " + String(statusCode))
break
case .networkError(let error):
debugPrint("Network error: " + error.localizedDescription)
break
case .cancelled(let error):
debugPrint("Request cancelled with error: " + error.localizedDescription)
break
default:
debugPrint("Error: " + error.localizedDescription)
}
}
UIImageView Extension
You can use the setRemoteImage method of UIImageView to download an image from a remote server
Example:
imageView.setRemoteImage(url: "http://www.website.com/image.jpg", defaultImage: UIImage(named: "DefaultImage"), beforeStart: {
imageView.activityIndicator.startAnimating()
}, preprocessImage: { image in // This block will run in background
let newImage = image.resize(100, 100)
return newImage
}) { image, error in
imageView.activityIndicator.stopAnimating()
}
Logging
You can turn on verbose mode to see what's going on in terminal for each request by setting the TNEnvironment.verbose to true
Tests
To run the tests open the Sample project, select Product -> Test or simply press ⌘U on keyboard.
TODO
- [x] Write test cases
- [x] Add support for request cancelation
- [x] Error handling
- [ ] Add support for downloading/uploading files
Contribution
Feel free to contribute to the project by creating a pull request and/or by reporting any issue(s) you find
Author
Bill Panagiotopoulos, [email protected]
Contributors
Alex Athanasiadis, [email protected]