iONess
iONess (iOS Network Session) is HTTP Request Helper for the iOS platform used by Home Credit Indonesia iOS App. It uses Ergo as a concurrent helper and promise pipelining.
Example
To run the example project, clone the repo, and run pod install
from the Example directory first.
Requirements
- Swift 5.0 or higher (or 5.1 when using Swift Package Manager)
- iOS 10 or higher (latest version)
- iOS 8 or higher (1.2.5 version)
Only Swift Package Manager
- macOS 10.10 or higher
- tvOS 10 or higher
Installation
Cocoapods
iONess is available through CocoaPods. To install it, simply add the following line to your Podfile:
for iOS 10 or higher
pod 'iONess', '~> 2.0'
or for iOS 8 or higher
pod 'iONess', '~> 1.2.5'
Swift Package Manager from XCode
- Add it using xcode menu File > Swift Package > Add Package Dependency
- Add https://github.com/oss-homecredit-id/iONess.git as Swift Package URL
- Set rules at version, with Up to Next Major option and put 2.0.2 as its version for iOS 10 or higher or 1.2.5 for iOS 8 or higher
- Click next and wait
Swift Package Manager from Package.swift
Add as your target dependency in Package.swift. Use 2.0.2 as its version for iOS 10 or higher or 1.2.5 for iOS 8 or higher
dependencies: [
.package(url: "https://github.com/oss-homecredit-id/iONess.git", .upToNextMajor(from: "2.0.2"))
]
Use it in your target as iONess
.target(
name: "MyModule",
dependencies: ["iONess"]
)
Contributor
- Home Credit Indonesia, iOS Teams
- nayanda, [email protected]
License
iONess is available under the MIT license. See the LICENSE file for more info.
Usage Example
Basic Usage
iONess
is designed to simplify the request process for HTTP Requests. All you need to do is just create the request using Ness
/ NetworkSessionManager
class:
Ness.default
.httpRequest(.get, withUrl: "https://myurl.com")
.dataRequest()
.then { result in
// do something with result this will not executed when request failed
}
or with no completion at all:
Ness.default
.httpRequest(.get, withUrl: "https://myurl.com")
.dataRequest()
When data dataRequest()
is called, it will always execute the request right away no matter it has completion or not. dataRequest()
actually returning Promise
object from Ergo so you could always do everything you can do with Ergo Promise
:
Ness.default
.httpRequest(.get, withUrl: "https://myurl.com")
.dataRequest()
.then { result in
// do something with result this will not executed when request failed
}.handle { error in
// do something if error occurs
}.finally { result, error in
// do something regarding of error or not after request completed
}
You could always check Ergo here about what its promise can do.
Create Request
To create a request you can do something like this:
Ness.default.httpRequest(.get, withUrl: "https://myurl.com")
.set(urlParameters: ["param1": "value1", "param2": "value2"])
.set(headers: ["Authorization": myToken])
.set(body: dataBody)
..
..
or with customize URLSession
:
// create session
var session = URLSession()
session.configuration = myCustomConfig
session.delegateQueue = myOperationQueue
..
..
// create Ness instance
let ness = Ness(with: session)
// create request
ness.httpRequest(.get, withUrl: "https://myurl.com")
.set(urlParameters: ["param1": "value1", "param2": "value2"])
.set(headers: ["Authorization": myToken])
.set(body: dataBody)
..
..
it's better to save the instance of Ness and reused it since it will be just creating the request with the same URLSession
unless you want to use any other URLSession
for another request.
available enumeration for HTTP Method to use are:
post
get
put
patch
delete
head
connect
options
trace
none
if you don't want to include HTTP Method headercustom(String)
to set a custom type of body, you need to pass those custom type encoder that implements HTTPBodyEncoder
object to encode the object into the data:
Ness.default.httpRequest(.get, withUrl: "https://myurl.com")
.set(body: myObject, with encoder: myEndoder) -> Self
..
..
The declaration of HTTPBodyEncoder
is:
public protocol HTTPBodyEncoder {
var relatedHeaders: [String: String]? { get }
func encoder(_ any: Any) throws -> Data
}
the relatedHeaders
is the associated header with this encoding which will be auto-assigned to the request headers. this variable is optional since the default implementation are returning nil
there some different default method to set the body with iONess default body encoder which are:
func set(body: Data) -> Self
func set(stringBody: String, encoding: String.Encoding = .utf8) -> Self
func set(jsonBody: [String: Any]) -> Self
func set(arrayJsonBody: [Any]) -> Self
func set<EObject: Encodable>(jsonEncodable: EObject) -> Self
func set<EObject: Encodable>(arrayJsonEncodable: [EObject]) -> Self
After the request is ready then prepare the request which will return Thenable:
Ness.default.httpRequest(.get, withUrl: "https://myurl.com")
.set(urlParameters: ["param1": "value1", "param2": "value2"])
.set(headers: ["Authorization": myToken])
.set(body: dataBody)
..
..
.dataRequest()
or for download, you need to give the target location URL
where you want to downloaded data to be saved:
Ness.default.httpRequest(.get, withUrl: "https://myurl.com")
.set(urlParameters: ["param1": "value1", "param2": "value2"])
.set(headers: ["Authorization": myToken])
.set(body: dataBody)
..
..
.downloadRequest(forSavedLocation: myTargetUrl)
or for upload you need to give file location URL
which you want to upload:
Ness.default.httpRequest(.get, withUrl: "https://myurl.com")
.set(urlParameters: ["param1": "value1", "param2": "value2"])
.set(headers: ["Authorization": myToken])
.set(body: dataBody)
..
..
.uploadRequest(forFileLocation: myTargetUrl)
Data Request Promise
After creating a data request, you can just execute the request with then method:
Ness.default
.httpRequest(.get, withUrl: "https://myurl.com")
..
..
.dataRequest()
.then { result in
// do something with result
}
The result is the URLResult
object which contains:
urlResponse: URLResponse?
which is the original response which you can read the documentation at hereerror: Error?
which is an error if happens. it will be nil on success responseresponseData: Data?
which is raw data of the response bodyisFailed: Bool
which is true if request is failedisSucceed: Bool
which is true if the request is succeedhttpMessage: HTTPResultMessage?
which is the response message of the request. It Will be nil if the result is not an HTTP result
The HTTPResultMessage
is the detailed HTTP response from the URLResult
:
url: HTTPURLCompatible
which is the origin URL of the responseheaders: Header
which is headers of the responsebody: Data?
which is the body of the responsestatusCode: Int
which is the status code of the response
You can get the promise object or ignore it. It will return DataPromise
which contains the status of the request
let requestPromise = Ness.default
.httpRequest(.get, withUrl: "https://myurl.com")
..
..
.dataRequest()
let status = requestPromise.status
The statuses are:
running(Float)
which contains the percentage of request progress from 0 - 1dropped
idle
completed(HTTPURLResponse)
which contains the completed responseerror(Error)
which contains an error if there are occurs
you can cancel the request using drop()
function:
requestPromise.drop()
since the promise is based on the Ergo Promise, it contains the result of the request if it already finished and an error if the error occurs:
// will be nil if the request is not finished yet or if the error occurs
let result = requestPromise.result
// will be nil if an error did not occur or the request is not finished yet
let error = requestPromise.error
// will be true if request completed
print(requestPromise.isCompleted)
Upload Request Promise
Upload requests are the same as Data requests in terms of Promise
.
Download Request Promise
Download requests have a slight difference from data requests or upload requests. The download request can be paused and resumed, and the result is different
The result is the DownloadResult
object which contains:
urlResponse: URLResponse?
which is the original response which you can read the documentation at hereerror: Error?
which is an error if happens. it will be nil on success responsedataLocalURL: URL?
which is the location of downloaded dataisFailed: Bool
which is true if request is failedisSucceed: Bool
which is true if the request is succeed
You can pause the download and resume:
request.pause()
let resumeStatus = request.resume()
resume will return ResumeStatus
which is enumeration:
resumed
failToResume
Decode Response Body For Data Request
to parse the body, you can do:
let decodedBody = try? result.message.parseBody(using: myDecoder)
the parseBody are accept any object that implement ResponseDecoder
. The declaration of ResponseDecoder protocol is like this:
public protocol ResponseDecoder {
associatedtype Decoded
func decode(from data: Data) throws -> Decoded
}
so you can do something like this:
class MyResponseDecoder: ResponseDecoder {
typealias Decoded = MyObject
func decode(from data: Data) throws -> MyObject {
// do something to decode data into MyObject
}
}
there are default base decoder you can use if you don't want to parse from Data
class MyJSONResponseDecoder: BaseJSONDecoder<MyObject> {
typealias Decoded = MyObject
override func decode(from json: [String: Any]) throws -> MyObject {
// do something to decode json into MyObject
}
}
class MyStringResponseDecoder: BaseStringDecoder<MyObject> {
typealias Decoded = MyObject
override func decode(from string: String) throws -> MyObject {
// do something to decode string into MyObject
}
}
the HTTPResultMessage
have default function to automatically parse the body which:
func parseBody(toStringEndcoded encoding: String.Encoding = .utf8) throws -> String
func parseJSONBody() throws -> [String: Any]
func parseArrayJSONBody() throws -> [Any]
func parseJSONBody<DObject: Decodable>() throws -> DObject
func parseArrayJSONBody<DObject: Decodable>() throws -> [DObject]
func parseJSONBody<DOBject: Decodable>(forType type: DOBject.Type) throws -> DOBject
func parseArrayJSONBody<DObject: Decodable>(forType type: DObject.Type) throws -> [DObject]
Validator
You can add validation for the response like this:
Ness.default
.httpRequest(.get, withUrl: "https://myurl.com")
..
..
.validate(statusCodes: 0..<300)
.validate(shouldHaveHeaders: ["Content-Type": "application/json"])
.dataRequest()
If the response is not valid, then it will have an error or be dispatched into handle
closure with an error.
the provided validate method are:
validate(statusCode: Int) -> Self
validate(statusCodes: Range<Int>) -> Self
validate(shouldHaveHeaders headers: [String:String]) -> Self
validate(_ validation: HeaderValidator.Validation, _ headers: [String: String]) -> Self
You can add custom validator to validate the http response. The type of validator is URLValidator
:
public protocol ResponseValidator {
func validate(for response: URLResponse) -> ResponseValidatorResult
}
ResponseValidatorResult
is a enumeration which contains:
valid
invalid
invalidWithReason(String)
invalid with custom reason which will be a description onNetworkSessionError
Error
and put your custom ResponseValidator
like this:
Ness.default
.httpRequest(.get, withUrl: "https://myurl.com")
..
..
.validate(using: MyCustomValidator())
.dataRequest()
You can use HTTPValidator
if you want to validate only HTTPURLResponse
and automatically invalidate the other:
public protocol HTTPValidator: URLValidator {
func validate(forHttp response: HTTPURLResponse) -> URLValidatorResult
}
Remember you can put as many validators as you want, which will validate the response using all those validators from the first until the end or until one validator returns invalid
If you don't provide any URLValidator
, then it will be considered invalid if there's an error or no response from the server, otherwise, all the responses will be considered valid
NetworkSessionManagerDelegate
You can manipulate request or action globally in Session level by using NetworkSessionManagerDelegate
:
public protocol NetworkSessionManagerDelegate: class {
func ness(_ manager: Ness, willRequest request: URLRequest) -> URLRequest
func ness(_ manager: Ness, didRequest request: URLRequest) -> Void
}
both methods are optional. The methods will run and functional for:
ness(_: , willRequest: )
will run before any request executed. You can manipulateURLRequest
object here and return it or do anything before request and return the currentURLRequest
ness(_: , didRequest: )
will run after any request is executed, but not after the request is finished.
RetryControl
You can control when to retry if your request is failed using RetryControl
protocol:
public protocol RetryControl {
func shouldRetry(
for request: URLRequest,
response: URLResponse?,
error: Error,
didHaveDecision: (RetryControlDecision) -> Void) -> Void
}
The method will run on a request failure. The only thing you need to do is pass the RetryControlDecision
into didHaveDecision
closure which is an enumeration with members:
noRetry
which will automatically fail the requestretryAfter(TimeInterval)
which will retry the same request afterTimeInterval
retry
which will retry the same request immediately
You can assign RetryControl
when preparing a request:
Ness.default
.httpRequest(.get, withUrl: "https://myurl.com")
..
..
.dataRequest(with: myRetryControl)
It can be applicable for download or upload requests too.
iONess has some default RetryControl
which is CounterRetryControl
that the basic algorithm is just counting the failure time and stop retry when the counter reaches the maxCount. to use it, just init the CounterRetryControl
when preparing with your maxCount or optionally with TimeInterval before retry. For example, if you want to auto-retry a maximum of 3 times with a delay of 1 second for every retry:
Ness.default
.httpRequest(.get, withUrl: "https://myurl.com")
..
..
.dataRequest(
with: CounterRetryControl(
maxRetryCount: 3,
timeIntervalBeforeTryToRetry: 1
)
)
DuplicatedHandler
You can handle what to do if there are multiple duplicated request happen with DuplicatedHandler
:
public protocol DuplicatedHandler {
func duplicatedDownload(request: URLRequest, withPreviousCompletion previousCompletion: @escaping URLCompletion<URL>, currentCompletion: @escaping URLCompletion<URL>) -> RequestDuplicatedDecision<URL>
func duplicatedUpload(request: URLRequest, withPreviousCompletion previousCompletion: @escaping URLCompletion<Data>, currentCompletion: @escaping URLCompletion<Data>) -> RequestDuplicatedDecision<Data>
func duplicatedData(request: URLRequest, withPreviousCompletion previousCompletion: @escaping URLCompletion<Data>, currentCompletion: @escaping URLCompletion<Data>) -> RequestDuplicatedDecision<Data>
}
It will ask for RequestDuplicatedDecision
depending on what type of duplicated request. The RequestDuplicatedDecision
are enumeration with members:
dropAndRequestAgain
which will drop the previous request and do a new request with the current completiondropAndRequestAgainWithCompletion((Param?, URLResponse?, Error?) -> Void)
which will drop previous request and do new request with custom completionignoreCurrentCompletion
which will ignore the current completion, so when the request is complete, it will just run the first request completionuseCurrentCompletion
which will ignore the previous completion, so when the request is complete, it will just run the lastest request completionuseBothCompletion
which will keep the previous completion, so when the request is complete, it will just run all the request completionuseCompletion((Param?, URLResponse?, Error?) -> Void)
which will ignore all completion and use the custom one
The duplicatedHandler is stick to the Ness
\ NetworkSessionManager
, so if you have duplicated request with different Ness
\ NetworkSessionManager
, it should not be called.
To assign RequestDuplicatedDecision
, you can just assign it to duplicatedHandler
property, or just add it when init:
// just handler
let ness = Ness(duplicatedHandler: myHandler)
// with session
let ness = Ness(session: mySession, duplicatedHandler: myHandler)
// using property
ness.duplicatedHandler = myHandler
Or you can just use some default handler:
// just handler
let ness = Ness(onDuplicated: .keepAllCompletion)
// with session
let ness = Ness(session: mySession, onDuplicated: .keepFirstCompletion)
// using property
ness.duplicatedHandler = DefaultDuplicatedHandler.keepLatestCompletion
There are 4 DefaultDuplicatedHandler
:
dropPreviousRequest
which will drop the previous request and do a new request with the current completionkeepAllCompletion
will keep the previous completion, so when the request is complete, it will just run all the request completionkeepFirstCompletion
which will ignore the current completion, so when the request is complete, it will just run the first request completionkeepLatestCompletion
which will ignore the previous completion, so when the request is complete, it will just run the lastest request completion
Contribute
You know how, just clone and do pull request