How to use dependency injection networking with MVVM iOS? - ios

I'm currently trying to find out what's the best networking architecture for MVVM applications. I couldn't find many resources and decided to go with dependency injection based architecture as per the very less reading resources that I have found.
I'm not using any 3rd party for web service testing and whatever the networking architecture that I use should be supported to mock the web services as well.
I have found out the DI based networking architecture which was build intended to achieve unit testing according to the Test Pyramid concept at Apple WWDC 2018.
So I have build my networking layer according to that. Following is my APIHandler class.
public enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case patch = "PATCH"
case delete = "DELETE"
}
protocol RequestHandler {
associatedtype RequestDataType
func makeRequest(from data:RequestDataType) -> URLRequest?
}
protocol ResponseHandler {
associatedtype ResponseDataType
func parseResponse(data: Data, response: HTTPURLResponse) throws -> ResponseDataType
}
typealias APIHandler = RequestHandler & ResponseHandler
Followings are my extensions for request handler and response handler.
extension RequestHandler {
func setQueryParams(parameters:[String: Any], url: URL) -> URL {
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
components?.queryItems = parameters.map { element in URLQueryItem(name: element.key, value: String(describing: element.value) ) }
return components?.url ?? url
}
func setDefaultHeaders(request: inout URLRequest) {
request.setValue(APIHeaders.contentTypeValue, forHTTPHeaderField: APIHeaders.kContentType)
}
}
struct ServiceError: Error,Decodable {
let httpStatus: Int
let message: String
}
extension ResponseHandler {
func defaultParseResponse<T: Decodable>(data: Data, response: HTTPURLResponse) throws -> T {
let jsonDecoder = JSONDecoder()
if response.statusCode == 200 {
do {
let body = try jsonDecoder.decode(T.self, from: data)
return body
} catch {
throw ServiceError(httpStatus: response.statusCode, message: error.localizedDescription)
}
} else {
var message = "Generel.Message.Error".localized()
do {
let body = try jsonDecoder.decode(APIError.self, from: data)
if let err = body.fault?.faultstring {
message = err
}
} catch {
throw ServiceError(httpStatus: response.statusCode, message: error.localizedDescription)
}
throw ServiceError(httpStatus: response.statusCode, message:message)
}
}
}
Then I loaded my request using APILoader as follows.
struct APILoader<T: APIHandler> {
var apiHandler: T
var urlSession: URLSession
init(apiHandler: T, urlSession: URLSession = .shared) {
self.apiHandler = apiHandler
self.urlSession = urlSession
}
func loadAPIRequest(requestData: T.RequestDataType, completionHandler: #escaping (Int, T.ResponseDataType?, ServiceError?) -> ()) {
if let urlRequest = apiHandler.makeRequest(from: requestData) {
urlSession.dataTask(with: urlRequest) { (data, response, error) in
if let httpResponse = response as? HTTPURLResponse {
guard error == nil else {
completionHandler(httpResponse.statusCode, nil, ServiceError(httpStatus: httpResponse.statusCode, message: error?.localizedDescription ?? "General.Error.Unknown".localized()))
return
}
guard let responseData = data else {
completionHandler(httpResponse.statusCode,nil, ServiceError(httpStatus: httpResponse.statusCode, message: error?.localizedDescription ?? "General.Error.Unknown".localized()))
return
}
do {
let parsedResponse = try self.apiHandler.parseResponse(data: responseData, response: httpResponse)
completionHandler(httpResponse.statusCode, parsedResponse, nil)
} catch {
completionHandler(httpResponse.statusCode, nil, ServiceError(httpStatus: httpResponse.statusCode, message: CommonUtil.shared.decodeError(err: error)))
}
} else {
guard error == nil else {
completionHandler(-1, nil, ServiceError(httpStatus: -1, message: error?.localizedDescription ?? "General.Error.Unknown".localized()))
return
}
completionHandler(-1, nil, ServiceError(httpStatus: -1, message: "General.Error.Unknown".localized()))
}
}.resume()
}
}
}
To call my API request. I have created a separate service class and call the web service as follows.
struct TopStoriesAPI: APIHandler {
func makeRequest(from param: [String: Any]) -> URLRequest? {
let urlString = APIPath().topStories
if var url = URL(string: urlString) {
if param.count > 0 {
url = setQueryParams(parameters: param, url: url)
}
var urlRequest = URLRequest(url: url)
setDefaultHeaders(request: &urlRequest)
urlRequest.httpMethod = HTTPMethod.get.rawValue
return urlRequest
}
return nil
}
func parseResponse(data: Data, response: HTTPURLResponse) throws -> StoriesResponse {
return try defaultParseResponse(data: data,response: response)
}
}
For syncing both my actual web service methods and mock services, I have created an API Client protocol like follows.
protocol APIClientProtocol {
func fetchTopStories(completion: #escaping (StoriesResponse?, ServiceError?) -> ())
}
Then I have derived APIServices class using my APIClient protocol and implemented my all the APIs there by passing requests and responses. My dependency injection was getting over at this point.
public class APIServices: APIClientProtocol {
func fetchTopStories(completion: #escaping (StoriesResponse?, ServiceError?) -> ()) {
let request = TopStoriesAPI()
let params = [Params.kApiKey.rawValue : CommonUtil.shared.NytApiKey()]
let apiLoader = APILoader(apiHandler: request)
apiLoader.loadAPIRequest(requestData: params) { (status, model, error) in
if let _ = error {
completion(nil, error)
} else {
completion(model, nil)
}
}
}
}
Then I have called this API request on my viewModel class like this.
func fetchTopStories(completion: #escaping (Bool) -> ()) {
APIServices().fetchTopStories { response, error in
if let _ = error {
self.errorMsg = error?.message ?? "Generel.Message.Error".localized()
completion(false)
} else {
if let data = response?.results {
if data.count > 0 {
self.stories.removeAll()
self.stories = data
completion(true)
} else {
self.errorMsg = "Generel.NoData.Error".localized()
completion(false)
}
} else {
self.errorMsg = "Generel.NoData.Error".localized()
completion(false)
}
}
}
}
Finally call the viewModel's API call from my viewController (View).
func fetchData() {
showActivityIndicator()
self.viewModel.fetchTopStories { success in
self.hideActivityIndicator()
DispatchQueue.main.async {
if self.pullToRefresh {
self.pullToRefresh = false
self.refreshControl.endRefreshing()
}
if success {
if self.imgNoData != nil {
self.imgNoData?.isHidden = true
}
self.tableView.reloadData()
} else {
CommonUtil.shared.showToast(message: self.viewModel.errorMsg, success: false)
self.imgNoData = {
let viewWidth = self.tableView.frame.size.width
let imageWidth = viewWidth - 50
let iv = UIImageView()
iv.frame = CGRect(x: 25, y: 100, width: imageWidth, height: imageWidth)
iv.image = UIImage(named:"no-data")
iv.contentMode = .scaleAspectFit
return iv
}()
self.imgNoData?.isHidden = false
self.tableView.addSubview(self.imgNoData!)
}
}
}
}
So I have following questions regarding this approach.
I have ended the dependency injection from my APIServices class.
Should I bring this all the way up to my viewController class API Call and
pass request and params variables from there ?
Are there any performance issues in this approach and any
improvement to be done?
My personal preference is to end all the data related stuffs from the viewModel level and just call the API without passing any parameters from the viewController. Does it wrong? If we pass parameters from the view controller class as per the pure dependency injection way, does it harm to the MVVM architecture?

Related

FlatMap with Generic ReturnType using Combine

I'm building a network API.
I'm new to Combine and I'm having some troubles with it, I'm trying to chain publish network requests, in this case I'm forming an URLRequest publisher and dispatching it on another publisher, the problem is that I cant make the flatMap work on the second publisher.
First I assemble the URLRequest with the Auth token:
func asURLRequest(baseURL: String) -> AnyPublisher<URLRequest, NetworkRequestError> {
return Deferred {
Future<URLRequest, NetworkRequestError> { promise in
if var urlComponents = URLComponents(string: baseURL) {
urlComponents.path = "\(urlComponents.path)\(path)"
urlComponents.queryItems = queryItemsFrom(params: queryParams)
if let finalURL = urlComponents.url {
if let user = Auth.auth().currentUser {
print("##### final url -> \(finalURL)")
// Retrieves the Firebase authentication token, possibly refreshing it if it has expired.
user.getIDToken(completion: { (token, error) in
if let fbToken = token {
var request = URLRequest(url: finalURL)
request.httpMethod = method.rawValue
request.httpBody = requestBodyFrom(params: body)
let defaultHeaders: HTTPHeaders = [
HTTPHeaderField.contentType.rawValue: contentType.rawValue,
HTTPHeaderField.acceptType.rawValue: contentType.rawValue,
HTTPHeaderField.authentication.rawValue: fbToken
]
request.allHTTPHeaderFields = defaultHeaders.merging(headers ?? [:], uniquingKeysWith: { (first, _) in first })
print("##### API TOKEN() SUCCESS: \(defaultHeaders)")
promise(.success(request))
}
if let fbError = error {
print("##### API TOKEN() ERROR: \(fbError)")
promise(.failure(NetworkRequestError.decodingError))
}
})
}
} else {
promise(.failure(NetworkRequestError.decodingError))
}
} else {
promise(.failure(NetworkRequestError.decodingError))
}
}
}.eraseToAnyPublisher()
}
Then I'm trying to dispatch a request (publisher) and return another publisher, the problem is that the .flatMap is not getting called:
struct APIClient {
var baseURL: String!
var networkDispatcher: NetworkDispatcher!
init(baseURL: String,
networkDispatcher: NetworkDispatcher = NetworkDispatcher()) {
self.baseURL = baseURL
self.networkDispatcher = networkDispatcher
}
/// Dispatches a Request and returns a publisher
/// - Parameter request: Request to Dispatch
/// - Returns: A publisher containing decoded data or an error
func dispatch<R: Request>(_ request: R) -> AnyPublisher<R.ReturnType, NetworkRequestError> {
print("##### --------> \(request)")
//typealias RequestPublisher = AnyPublisher<R.ReturnType, NetworkRequestError>
return request.asURLRequest(baseURL: baseURL)
.flatMap { request in
//NOT GETTING CALLED
self.networkDispatcher.dispatch(request: request)
}.eraseToAnyPublisher()
}
The final publisher that is not being called is the following:
struct NetworkDispatcher {
let urlSession: URLSession!
public init(urlSession: URLSession = .shared) {
self.urlSession = urlSession
}
/// Dispatches an URLRequest and returns a publisher
/// - Parameter request: URLRequest
/// - Returns: A publisher with the provided decoded data or an error
func dispatch<ReturnType: Codable>(request: URLRequest) -> AnyPublisher<ReturnType, NetworkRequestError> {
return urlSession
.dataTaskPublisher(for: request)
// Map on Request response
.tryMap({ data, response in
// If the response is invalid, throw an error
if let response = response as? HTTPURLResponse,
!(200...299).contains(response.statusCode) {
throw httpError(response.statusCode)
}
// Return Response data
return data
})
// Decode data using our ReturnType
.decode(type: ReturnType.self, decoder: JSONDecoder())
// Handle any decoding errors
.mapError { error in
handleError(error)
}
// And finally, expose our publisher
.eraseToAnyPublisher()
}
}
Running the code:
struct ReadUser: Request {
typealias ReturnType = UserData
var path: String
var method: HTTPMethod = .get
init(_ id: String) {
path = "users/\(id)"
}
}
let apiClient = APIClient(baseURL: BASE_URL)
var cancellables = [AnyCancellable]()
apiClient.dispatch(ReadUser(Auth.auth().currentUser?.uid ?? ""))
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { result in
switch result {
case .failure(let error):
// Handle API response errors here (WKNetworkRequestError)
print("##### Error loading data: \(error)")
default: break
}
},
receiveValue: { value in
})
.store(in: &cancellables)
I took your code and boiled it down to just the Combine parts. I could not reproduce the issue you are describing. I'll post that code below. I recommend you start simplifying your code a bit at a time to see if that helps. Factoring out the Auth and Facebook token code seems like a good candidate to start with. Another good debugging technique might be to put in more explicit type declarations to make sure your closures are taking and returning what you expect. (just the other day I had a map that I thought I was applying to an Array when I was really mapping over Optional).
Here's the playground:
import UIKit
import Combine
func asURLRequest(baseURL: String) -> AnyPublisher<URLRequest, Error> {
return Deferred {
Future<URLRequest, Error> { promise in
promise(.success(URLRequest(url: URL(string: "https://www.apple.com")!)))
}
}.eraseToAnyPublisher()
}
struct APIClient {
var networkDispatcher: NetworkDispatcher!
init(networkDispatcher: NetworkDispatcher = NetworkDispatcher()) {
self.networkDispatcher = networkDispatcher
}
func dispatch() -> AnyPublisher<Data, Error> {
return asURLRequest(baseURL: "Boo!")
.flatMap { (request: URLRequest) -> AnyPublisher<Data, Error> in
print("Request Received. \(String(describing: request))")
return self.networkDispatcher.dispatch(request: request)
}.eraseToAnyPublisher()
}
}
func httpError(_ code: Int) -> Error {
return NSError(domain: "Bad Things", code: -1, userInfo: nil)
}
func handleError(_ error: Error) -> Error {
debugPrint(error)
return error
}
struct NetworkDispatcher {
let urlSession: URLSession!
public init(urlSession: URLSession = .shared) {
self.urlSession = urlSession
}
func dispatch(request: URLRequest) -> AnyPublisher<Data, Error> {
return urlSession
.dataTaskPublisher(for: request)
.tryMap({ data, response in
if let response = response as? HTTPURLResponse,
!(200...299).contains(response.statusCode) {
throw httpError(response.statusCode)
}
// Return Response data
return data
})
.mapError { error in
handleError(error)
}
.eraseToAnyPublisher()
}
}
let apiClient = APIClient()
var cancellables = [AnyCancellable]()
apiClient.dispatch()
.print()
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { result in
debugPrint(result)
switch result {
case .failure(let error):
// Handle API response errors here (WKNetworkRequestError)
print("##### Error loading data: \(error)")
default: break
}
},
receiveValue: { value in
debugPrint(value)
})
.store(in: &cancellables)
I refactored your code. Breaking down the offending method into several functions. I could not find any problem. Below is my refactoring. You will notice that I broke all the code that constructs things into their own functions so they can be easily tested without dealing with the effect (I don't even have to mock the effect to test the logic.)
extension Request {
func asURLRequest(baseURL: String) -> AnyPublisher<URLRequest, NetworkRequestError> {
guard let user = Auth.auth().currentUser else {
return Fail(error: NetworkRequestError.missingUser)
.eraseToAnyPublisher()
}
return user.idTokenPublisher()
.catch { error in
Fail(error: NetworkRequestError.badToken(error))
}
.tryMap { token in
makeRequest(
finalURL: try finalURL(baseURL: baseURL),
fbToken: token
)
}
.eraseToAnyPublisher()
}
func finalURL(baseURL: String) throws -> URL {
guard var urlComponents = URLComponents(string: baseURL) else {
throw NetworkRequestError.malformedURLComponents
}
urlComponents.path = "\(urlComponents.path)\(path)"
urlComponents.queryItems = queryItemsFrom(params: queryParams)
guard let result = urlComponents.url else {
throw NetworkRequestError.malformedURLComponents
}
return result
}
func makeRequest(finalURL: URL, fbToken: String) -> URLRequest {
var request = URLRequest(url: finalURL)
request.httpMethod = method.rawValue
request.httpBody = requestBodyFrom(params: body)
let defaultHeaders: HTTPHeaders = [
HTTPHeaderField.contentType.rawValue: contentType.rawValue,
HTTPHeaderField.acceptType.rawValue: contentType.rawValue,
HTTPHeaderField.authentication.rawValue: fbToken
]
request.allHTTPHeaderFields = defaultHeaders.merging(
headers ?? [:],
uniquingKeysWith: { (first, _) in first }
)
return request
}
}
extension User {
func idTokenPublisher() -> AnyPublisher<String, Error> {
Deferred {
Future { promise in
getIDToken(completion: { token, error in
if let token = token {
promise(.success(token))
}
else {
promise(.failure(error ?? UnknownError()))
}
})
}
}
.eraseToAnyPublisher()
}
}
struct UnknownError: Error { }

Codable for API request

How would I make this same API request through codables?
In my app, this function is repeated in every view that makes API calls.
func getOrders() {
DispatchQueue.main.async {
let spinningHUD = MBProgressHUD.showAdded(to: self.view, animated: true)
spinningHUD.isUserInteractionEnabled = false
let returnAccessToken: String? = UserDefaults.standard.object(forKey: "accessToken") as? String
let access = returnAccessToken!
let headers = [
"postman-token": "dded3e97-77a5-5632-93b7-dec77d26ba99",
"Authorization": "JWT \(access)"
]
let request = NSMutableURLRequest(url: NSURL(string: "https://somelink.com")! as URL,
cachePolicy: .useProtocolCachePolicy,
timeoutInterval: 10.0)
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers
let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
if (error != nil) {
print(error!)
} else {
if let dataNew = data, let responseString = String(data: dataNew, encoding: .utf8) {
print("----- Orders -----")
print(responseString)
print("----------")
let dict = self.convertToDictionary(text: responseString)
print(dict?["results"] as Any)
guard let results = dict?["results"] as? NSArray else { return }
self.responseArray = (results) as! [HomeVCDataSource.JSONDictionary]
DispatchQueue.main.async {
spinningHUD.hide(animated: true)
self.tableView.reloadData()
}
}
}
})
dataTask.resume()
}
}
I would suggest to do the following
Create Base Service as below
import UIKit
import Foundation
enum MethodType: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case patch = "PATCH"
case delete = "DELETE"
}
class BaseService {
var session: URLSession!
// MARK: Rebuilt Methods
func FireGenericRequest<ResponseModel: Codable>(url: String, methodType: MethodType, headers: [String: String]?, completion: #escaping ((ResponseModel?) -> Void)) {
UIApplication.shared.isNetworkActivityIndicatorVisible = true
// Request Preparation
guard let serviceUrl = URL(string: url) else {
print("Error Building URL Object")
return
}
var request = URLRequest(url: serviceUrl)
request.httpMethod = methodType.rawValue
// Header Preparation
if let header = headers {
for (key, value) in header {
request.setValue(value, forHTTPHeaderField: key)
}
}
// Firing the request
session = URLSession(configuration: URLSessionConfiguration.default)
session.dataTask(with: request) { (data, response, error) in
DispatchQueue.main.async {
UIApplication.shared.isNetworkActivityIndicatorVisible = false
}
if let data = data {
do {
guard let object = try? JSONDecoder().decode(ResponseModel.self , from: data) else {
print("Error Decoding Response Model Object")
return
}
DispatchQueue.main.async {
completion(object)
}
}
}
}.resume()
}
private func buildGenericParameterFrom<RequestModel: Codable>(model: RequestModel?) -> [String : AnyObject]? {
var object: [String : AnyObject] = [String : AnyObject]()
do {
if let dataFromObject = try? JSONEncoder().encode(model) {
object = try JSONSerialization.jsonObject(with: dataFromObject, options: []) as! [String : AnyObject]
}
} catch (let error) {
print("\nError Encoding Parameter Model Object \n \(error.localizedDescription)\n")
}
return object
}
}
the above class you may reuse it in different scenarios adding request object to it and passing any class you would like as long as you are conforming to Coddle protocol
Create Model Conforming to Coddle protocol
class ExampleModel: Codable {
var commentId : String?
var content : String?
//if your JSON keys are different than your property name
enum CodingKeys: String, CodingKey {
case commentId = "CommentId"
case content = "Content"
}
}
Create Service to the specific model with the endpoint constants subclassing to BaseService as below
class ExampleModelService: BaseService<ExampleModel/* or [ExampleModel]*/> {
func GetExampleModelList(completion: ((ExampleModel?)/* or [ExampleModel]*/ -> Void)?) {
super.FireRequestWithURLSession(url: /* url here */, methodType: /* method type here */, headers: /* headers here */) { (responseModel) in
completion?(responseModel)
}
}
}
Usage
class MyLocationsController: UIViewController {
// MARK: Properties
// better to have in base class for the controller
var exampleModelService: ExampleModelService = ExampleModelService()
// MARK: Life Cycle Methods
override func viewDidLoad() {
super.viewDidLoad()
exampleModelService.GetExampleModelList(completion: { [weak self] (response) in
// model available here
})
}
}
Basically, you need to conform Codable protocol in your model classes, for this you need to implement 2 methods, one for code your model and another for decode your model from JSON
func encode(to encoder: Encoder) throws
required convenience init(from decoder: Decoder) throws
After that you will be able to use JSONDecoder class provided by apple to decode your JSON, and return an array (if were the case) or an object of your model class.
class ExampleModel: Codable {
var commentId : String?
var content : String?
//if your JSON keys are different than your property name
enum CodingKeys: String, CodingKey {
case commentId = "CommentId"
case content = "Content"
}
}
Then using JSONDecoder you can get your model array like this
do {
var arrayOfOrders : [ExampleModel] = try JSONDecoder().decode([ExampleModel].self, from: dataNew)
}
catch {
}
First of all, I can recommend you to use this application -quicktype- for turning json file to class or struct (codable) whatever you want. enter link description here.
After that you can create a generic function to get any kind of codable class and return that as a response.
func taskHandler<T:Codable>(type: T.Type, useCache: Bool, urlRequest: URLRequest, completion: #escaping (Result<T, Error>) -> Void) {
let task = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
if let error = error {
print("error : \(error)")
}
if let data = data {
do {
let dataDecoded = try JSONDecoder().decode(T.self, from: data)
completion(.success(dataDecoded))
// if says use cache, let's store response data to cache
if useCache {
if let response = response as? HTTPURLResponse {
self.storeDataToCache(urlResponse: response, urlRequest: urlRequest, data: data)
}
}
} catch let error {
completion(.failure(error))
}
} else {
completion(.failure(SomeError))
}
}
task.resume()
}

How can I unit test a network request using a local json file?

I'm trying to figure out the best way to unit test a network request. My initial thought was to create a local file with the JSON response for testing purposes but that doesn't seem to be working. See my code below.
I wanna test that I can get a non-nil array back from the completion handler in the function below.
class APIClient {
let downloader = JSONDownloader() // just a class that creates a new data task
// what I want to test
func getArticles(from url: URL?, completion: #escaping([Article]?, Error?) -> ()) {
guard let url = url else { return }
let request = URLRequest(url: url)
let task = downloader.createTask(with: request) { json, error in
DispatchQueue.main.async {
// parse JSON
...
completion(articles, nil)
}
}
task.resume()
}
}
I tried testing as shown below to no avail.
func testArticleResponseIsNotNil() {
let bundle = Bundle(for: APIClientTests.self)
guard let path = Bundle.path(forResource: "response-articles", ofType: "json", inDirectory: bundle.bundlePath) else {
XCTFail("Missing file: response-articles.json")
return
}
let url = URL(fileURLWithPath: path)
var articles: [Article]?
let expectation = self.expectation(description: "Articles")
let client = APIClient()
client.getArticles(from: url) { response, error in
articles = response
expectation.fulfill()
}
wait(for: [expectation], timeout: 5)
XCTAssertNotNil(articles)
}
Any ideas on how exactly I should test this function?
Edit: This is the JSONDownloader class.
class JSONDownloader {
let session: URLSession
init(configuration: URLSessionConfiguration) {
self.session = URLSession(configuration: configuration)
}
convenience init() {
self.init(configuration: .default)
}
typealias JSON = [String: AnyObject]
func createTask(with request: URLRequest, completion: #escaping(JSON?, Error?) -> ()) -> URLSessionDataTask {
let task = session.dataTask(with: request) { data, response, error in
guard let httpResponse = response as? HTTPURLResponse else { return }
if httpResponse.statusCode == 200 {
if let data = data {
do {
let json = try JSONSerialization.jsonObject(with: data, options: []) as? JSON
completion(json, nil)
} catch { completion(nil, error) }
} else { completion(nil, error) }
} else { completion(nil, error) }
}
return task
}
}

iOS - Alamofire RequestRetrier not provoked

I am trying to implement a retry mechanism and i saw that alamofire has one.
I am trying to implement a simple mechanism of retry with number of times for a request , yet something is wrong.
class OAuth2Handler: RequestAdapter, RequestRetrier {
func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
return urlRequest
}
var defaultRetryCount = 4
private var requestsAndRetryCounts: [(Request, Int)] = []
private var lock = NSLock()
private func index(request: Request) -> Int? {
return requestsAndRetryCounts.index(where: { $0.0 === request })
}
func addRetryInfo(request: Request, retryCount: Int? = nil) {
lock.lock() ; defer { lock.unlock() }
guard index(request: request) == nil else { print("ERROR addRetryInfo called for already tracked request"); return }
requestsAndRetryCounts.append((request, retryCount ?? defaultRetryCount))
}
func deleteRetryInfo(request: Request) {
lock.lock() ; defer { lock.unlock() }
guard let index = index(request: request) else { print("ERROR deleteRetryInfo called for not tracked request"); return }
requestsAndRetryCounts.remove(at: index)
}
func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: #escaping RequestRetryCompletion){
lock.lock() ; defer { lock.unlock() }
guard let index = index(request: request) else { completion(false, 0); return }
let (request, retryCount) = requestsAndRetryCounts[index]
if retryCount == 0 {
completion(false, 0)
} else {
requestsAndRetryCounts[index] = (request, retryCount - 1)
completion(true, 0.5)
}
}
}
this is the class that i am trying to use this:
let sessionManager = SessionManager()
override init() {
sessionManager.adapter = RequestAdapter.self as? RequestAdapter
sessionManager.retrier = OAuth2Handler()
}
func sendRequest(url: String,meth: HTTPMethod,parameters: [String: AnyObject]?, success: #escaping (String, Data) -> Void, failure: #escaping (Error) -> Void) {
self.asyncSerialWorker.enqueueWork { (done) in
self.sessionManager.request(url, method:meth).responseJSON { (responseObject) -> Void in
if responseObject.result.isSuccess {
print("Generic succsess")
let value = responseObject.result.value
let json = JSON(value!)
guard let result = responseObject.data else {return}
success(self.parser.parseMaiden(json: json), result)
}
if responseObject.result.isFailure {
let error : Error = responseObject.result.error!
print("login failed")
failure(error)
}
done()
}
}
}
if there are any other suggestions i would love to hear them
thanks
sessionManager.adapter = RequestAdapter.self as? RequestAdapter seems very wrong. You should be setting it to an instance of your OAuth2Handler.
So the issue her was to add the request to the retry, so first i did this:
let sessionManager = SessionManager()
var retrier = OAuth2Handler()
override init() {
sessionManager.retrier = retrier
}
and in the call itself i did as follow:
func sendRequest(url: String,meth: HTTPMethod,parameters: [String: AnyObject]?, success: #escaping (String, Data) -> Void, failure: #escaping (Error) -> Void) {
let request = sessionManager.request(url, method: meth, parameters: parameters, encoding: JSONEncoding.default)
retrier.addRetryInfo(request: request)
self.asyncSerialWorker.enqueueWork { (done) in
self.sessionManager.request(url, method:meth).responseJSON { (responseObject) -> Void in
if responseObject.result.isSuccess {
print("Generic succsess")
let value = responseObject.result.value
let json = JSON(value!)
guard let result = responseObject.data else {return}
success(self.parser.parseMaiden(json: json), result)
}
if responseObject.result.isFailure {
let error : Error = responseObject.result.error!
print("login failed")
failure(error)
}
done()
}
}
}
as you can see i have add to the retry a request :
retrier.addRetryInfo(request: request)
maybe i should do a remove in success(will check and update)

How to get Xcode server Code coverage api JSON response

When i try to hit Xcode server code coverage API by passing integration ID, instead of JSON response it is downloading a .bz2 file directly. I want to show the file wise coverage report in my custom dashboard using this API.
Is there any way i can get JSOn response from this API (https://developer.apple.com/library/content/documentation/Xcode/Conceptual/XcodeServerAPIReference/CodeCoverage.html) instead of .bz2 file?
Unfortunately, the API only returns the .bz2 compressed JSON file. Even when specifying a HTTP Header of Accept=application/json.
The only way around this is to decompress the data to access the underlying JSON.
Here's an example of what this could look like on iOS/swift using the framework BZipCompression to decompress the data stream:
import Foundation
import BZipCompression
public class Coverage {
public typealias CoverageCompletion = (_: Data?, _: Error?) -> Void
public enum Errors: Error {
case invalidURL
case invalidResponse
case invalidStatusCode
case invalidData
}
static var session: URLSession {
let session = URLSession(configuration: URLSessionConfiguration.default, delegate: LocalhostSessionDelegate.default, delegateQueue: nil)
return session
}
static public func coverage(forIntegrationWithIdentifier identifier: String, completion: #escaping CoverageCompletion) {
guard let url = URL(string: "https://localhost:20343/api/integrations/\(identifier)/coverage") else {
completion(nil, Errors.invalidURL)
return
}
let request = URLRequest(url: url)
let task = session.dataTask(with: request) { (data, response, error) in
guard error == nil else {
completion(nil, error)
return
}
guard let urlResponse = response as? HTTPURLResponse else {
completion(nil, Errors.invalidResponse)
return
}
guard urlResponse.statusCode == 200 else {
completion(nil, Errors.invalidStatusCode)
return
}
guard let d = data else {
completion(nil, Errors.invalidData)
return
}
var decompressedData: Data
do {
decompressedData = try self.decompress(data: d)
} catch let decompressionError {
completion(nil, decompressionError)
return
}
completion(decompressedData, nil)
}
task.resume()
}
static internal func decompress(data: Data) throws -> Data {
let decompressedData = try BZipCompression.decompressedData(with: data)
guard let decompressedString = String(data: decompressedData, encoding: .utf8) else {
throw Errors.invalidData
}
guard let firstBrace = decompressedString.range(of: "{") else {
throw Errors.invalidData
}
guard let lastBrace = decompressedString.range(of: "}", options: .backwards, range: nil, locale: nil) else {
throw Errors.invalidData
}
let range = decompressedString.index(firstBrace.lowerBound, offsetBy: 0)..<decompressedString.index(lastBrace.lowerBound, offsetBy: 1)
let json = decompressedString.substring(with: range)
guard let validData = json.data(using: .utf8) else {
throw Errors.invalidData
}
return validData
}
}
/// Class implementing the NSURLSessionDelegate which forcefully bypasses untrusted SSL Certificates.
public class LocalhostSessionDelegate: NSObject, URLSessionDelegate {
static public var `default` = LocalhostSessionDelegate()
// MARK: - NSURLSessionDelegate
#objc open func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard challenge.previousFailureCount < 1 else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
var credentials: URLCredential?
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
if let serverTrust = challenge.protectionSpace.serverTrust {
credentials = URLCredential(trust: serverTrust)
}
}
completionHandler(.useCredential, credentials)
}
}
I've noticed that the decompressed data often includes invalid control characters and other garbage at the beginning and end of the valid JSON block. The decompress() cleans up the data before returning it in the completion block.
You may want to check out my swift XCServerAPI framework on GitHub. I'll be adding the Code Coverage endpoint with this exact solution.

Resources