Lightweight and easy to use network abstraction layer that sufficiently encapsulates URLSession
- Execute HTTP requests
- Mock responses
- Intercept request/response
- SSL pinning configuration
- [WIP] Response custom DispatchQueue
- [WIP] Integrated JSON decoder in Response object
- iOS 12+ / macOS 10.14+ / watchOS 5+ / tvOS 12+
- Xcode 11.0+
- Swift 5+
Add this swift package to your project
https://auk-tfs.nzlabs.net/tfs/DefaultCollection/Auckland_PD/_git/iOS_NetworkKit
Provide URLSessionConfiguration object to the NetworkKit before the actual usage. If no configuration is set, NetworkKit will use the default.
let config = URLSessionConfiguration()
NetworkKit.shared.setup(with: config)
SSL pinning can be set up by providing SSLPinning object during NetworkKit setup proccess.
let config = SSLPingningConfiguration(sslCertificates: [certificate], sslPinningCertificateType: .root)
let sslPinning = SSLPining(sslPinningConfig: config)
NetworkKit.shared.setup(sslPingning: sslPinning)
There is an EndpointType interface that describes endpoint behaviour such as path, http method, task, mock data, etc. You can use struct/enum for the endpoints you create. If you have multiple requests to one specific endpoint, use enum with cases. Despite, single request endpoint could be achieved with using struct.
enum TestEndpoint: EndpointType {
case accounts
case transfers
var baseURL: URL {
return URL(string: "https://example-url")!
}
var path: String {
switch self {
case .accounts:
return "accounts"
case .transfers:
return "transfers"
}
}
var httpMethod: HTTPMethod {
return .get
}
var task: HTTPTask {
return .requestPlain
}
var headers: HTTPHeaders {
return [:]
}
var mockResponse: MockResponseType? {
switch self {
case .accounts:
let jsonString = "{ \"message\": \"Accounts response\" }\n"
let data = jsonString.data(using: .utf8)!
return .response(201, data)
case .transfers:
let jsonString = "{ \"message\": \"Transfers response\" }\n"
let data = jsonString.data(using: .utf8)!
return .response(201, data)
}
}
}
.requestPlain type is used for the request where we don't need to attach any data
.requestData(Data) type is used to attach Data object to request httpBody
.requestJSONEncodable(Encodable) type is used to attach JSON Encodable object to request httpBody
.requestParameters(_ parameters: Parameters,_ encodingDestination: URLEncoding.Destination) is used to attach URL encoded string to either url or httpBody of the request.
Note: it's required to set headers manually to the endpoint for URL encoding or JSON formats. For example:
var headers: HTTPHeaders {
return ["Content-Type": "application/x-www-form-urlencoded"]
}
or
var headers: HTTPHeaders {
return ["Content-Type": "application/json"]
}
There is an DefaultNetworkRouter object that handles endpoint request execution. Every router is bind to the EndpointType. This is primarly done to prevent users from using single router for different endpoints. Important each router instance can handle 1 request at a time. if you need to exceute multiple requests simultaneously, create multiple routers for every request.
let router = DefaultNetworkRouter<TestEndpoint>()
To enable mocks, just simply specify MockBehavior parameter in the router constructor.
let router = DefaultNetworkRouter<TestEndpoint>(mockBehavior: .immediate)
Note: if mockResponse is not specified in the endpoint, router will crash with fatal error using .immediate or .delayed(_) MockBehavior type.
There is an Interceptor interface that should be used for request and response modifications.
struct AuthInterceptor: Interceptor {
func prepare(_ request: inout URLRequest) {
request.addValue("Bearer token1", forHTTPHeaderField: "Authorization")
}
func process(_ result: inout Result<Response, NetworkError>) {
guard case .success(let response) = result else {
return
}
var anyHashableHeaders = response.httpURLResponse?.allHeaderFields ?? [AnyHashable: Any]()
anyHashableHeaders["Authorization"] = "Bearer token2"
guard let headers = anyHashableHeaders as? [String: String] else {
return
}
let httpURLResponse = HTTPURLResponse(url: response.urlRequest.url!,
statusCode: 200,
httpVersion: nil,
headerFields: headers)
let newResponse = Response(urlRequest: response.urlRequest, data: response.data!, httpURLResponse: httpURLResponse)
result = .success(newResponse)
}
}
You can pass array of interceptors to the router constructor.
let router = DefaultNetworkRouter<TestEndpoint>(interceptors: [authInterceptor], mockBehavior: .immediate)
Call execute method for router object to run your request. You are going to get result of Result<Response, NetworkError>) type. Response object contains URLRequest, Data and HTTPURLResponse objects.
router.execute(endpoint: TestEndpoint.test, completion: { result in
switch result {
case .success(let response):
// handle response
case .failure(let error):
// handle error
}
})
To decode JSON response, use Swift JSONDecoder.
let decoder = JSONDecoder()
let object = try decoder.decode(Object.self, response.data)
You can filter the response by status codes range, or filter successful status codes only by calling filterSuccessfulStatusCodes() method.
do {
try response.filterSuccessfulStatusCodes()
} ctach let error {
// handle error
}
To cancel request, just simply call cancel method for router object. By cancelling the request, you're going to recieved NSURLErrorDomain error.
router.cancel()
There is a NetworkError type error that you can recieve during request execution. If you need to handle one particular error case, you can use guard case statement.
guard case .underlying(let networkError, let response) = error else {
return
}
Use errorDescription variable of NetworkError to print localised description.
print(error.errorDescription)
Read NetworkError documentation for more details.