diff --git a/SWDestinyTrades/Classes/AddCard/Controller/AddCardViewController.swift b/SWDestinyTrades/Classes/AddCard/Controller/AddCardViewController.swift index fcc143d5..4319a078 100644 --- a/SWDestinyTrades/Classes/AddCard/Controller/AddCardViewController.swift +++ b/SWDestinyTrades/Classes/AddCard/Controller/AddCardViewController.swift @@ -54,18 +54,7 @@ final class AddCardViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - addCardView.startLoading() - destinyService.retrieveAllCards { [weak self] result in - self?.addCardView.stopLoading() - switch result { - case let .success(allCards): - self?.addCardView.updateSearchList(allCards) - self?.cards = allCards - case let .failure(error): - ToastMessages.showNetworkErrorMessage() - LoggerManager.shared.log(event: .allCards, parameters: ["error": error.localizedDescription]) - } - } + fetchAllCards() addCardView.didSelectCard = { [weak self] card in self?.insert(card: card) @@ -87,6 +76,26 @@ final class AddCardViewController: UIViewController { // MARK: - Helpers + private func fetchAllCards() { + addCardView.startLoading() + Task { [weak self] in + guard let self else { return } + + defer { + self.addCardView.stopLoading() + } + + do { + let allCards = try await self.destinyService.retrieveAllCards() + self.addCardView.updateSearchList(allCards) + self.cards = allCards + } catch { + ToastMessages.showNetworkErrorMessage() + LoggerManager.shared.log(event: .allCards, parameters: ["error": error.localizedDescription]) + } + } + } + private func insert(card: CardDTO) { switch addCardType { case .lent: diff --git a/SWDestinyTrades/Classes/AddToDeck/Controller/AddToDeckViewController.swift b/SWDestinyTrades/Classes/AddToDeck/Controller/AddToDeckViewController.swift index 4b556f7b..fe8504b2 100644 --- a/SWDestinyTrades/Classes/AddToDeck/Controller/AddToDeckViewController.swift +++ b/SWDestinyTrades/Classes/AddToDeck/Controller/AddToDeckViewController.swift @@ -67,14 +67,22 @@ final class AddToDeckViewController: UIViewController { private func retrieveAllCards() { addToDeckView.activityIndicator.startAnimating() - destinyService.retrieveAllCards { [weak self] result in - self?.addToDeckView.activityIndicator.stopAnimating() - switch result { - case let .success(allCards): - self?.addToDeckView.addToDeckTableView.updateSearchList(allCards) - self?.cards = allCards - case let .failure(error): - self?.handleFailure(error) + Task { [weak self] in + guard let self else { return } + + defer { + self.addToDeckView.activityIndicator.stopAnimating() + } + + do { + let allCards = try await self.destinyService.retrieveAllCards() + self.addToDeckView.addToDeckTableView.updateSearchList(allCards) + self.cards = allCards + } catch APIError.requestCancelled { + // do nothing + } catch { + ToastMessages.showNetworkErrorMessage() + LoggerManager.shared.log(event: .allCards, parameters: ["error": error.localizedDescription]) } } } @@ -115,16 +123,6 @@ final class AddToDeckViewController: UIViewController { LoadingHUD.show(.labeledSuccess(title: L10n.added, subtitle: card.name)) } - private func handleFailure(_ error: APIError) { - switch error { - case .requestCancelled: - break - default: - ToastMessages.showNetworkErrorMessage() - LoggerManager.shared.log(event: .allCards, parameters: ["error": error.localizedDescription]) - } - } - // MARK: - Navigation func navigateToNextController(with card: CardDTO) { diff --git a/SWDestinyTrades/Classes/CardList/Controller/CardListViewController.swift b/SWDestinyTrades/Classes/CardList/Controller/CardListViewController.swift index d2407743..7a12d626 100644 --- a/SWDestinyTrades/Classes/CardList/Controller/CardListViewController.swift +++ b/SWDestinyTrades/Classes/CardList/Controller/CardListViewController.swift @@ -36,17 +36,8 @@ final class CardListViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - cardListView.activityIndicator.startAnimating() - destinyService.retrieveSetCardList(setCode: setDTO.code.lowercased()) { [weak self] result in - self?.cardListView.activityIndicator.stopAnimating() - switch result { - case let .success(cardList): - self?.cardListView.cardListTableView.updateCardList(cardList) - case let .failure(error): - ToastMessages.showNetworkErrorMessage() - LoggerManager.shared.log(event: .cardsList, parameters: ["error": error.localizedDescription]) - } - } + fetchSetCardList() + cardListView.cardListTableView.didSelectCard = { [weak self] list, card in self?.navigateToNextController(cardList: list, card: card) } @@ -58,6 +49,25 @@ final class CardListViewController: UIViewController { navigationItem.title = setDTO.name } + private func fetchSetCardList() { + cardListView.activityIndicator.startAnimating() + Task { [weak self] in + guard let self else { return } + + defer { + self.cardListView.activityIndicator.stopAnimating() + } + + do { + let cardList = try await self.destinyService.retrieveSetCardList(setCode: self.setDTO.code.lowercased()) + self.cardListView.cardListTableView.updateCardList(cardList) + } catch { + ToastMessages.showNetworkErrorMessage() + LoggerManager.shared.log(event: .cardsList, parameters: ["error": error.localizedDescription]) + } + } + } + // MARK: - Navigation func navigateToNextController(cardList: [CardDTO], card: CardDTO) { diff --git a/SWDestinyTrades/Classes/Rest/Base/APIError.swift b/SWDestinyTrades/Classes/Rest/Base/APIError.swift index 596da630..6584f691 100644 --- a/SWDestinyTrades/Classes/Rest/Base/APIError.swift +++ b/SWDestinyTrades/Classes/Rest/Base/APIError.swift @@ -10,11 +10,15 @@ import Foundation enum APIError: Error { case requestFailed(reason: String?) - case jsonConversionFailure + case jsonConversionFailure(domain: String, description: String) case invalidData case responseUnsuccessful case jsonParsingFailure case requestCancelled + case keyNotFound(key: CodingKey, context: String) + case valueNotFound(type: Any.Type, context: String) + case typeMismatch(type: Any.Type, context: String) + case dataCorrupted(context: String) var localizedDescription: String { switch self { @@ -26,10 +30,18 @@ enum APIError: Error { return "Response Unsuccessful" case .jsonParsingFailure: return "JSON Parsing Failure" - case .jsonConversionFailure: - return "JSON Conversion Failure" + case let .jsonConversionFailure(domain, description): + return "Error in read(from:ofType:) domain= \(domain), description= \(description)" case .requestCancelled: return "Request Cancelled" + case let .keyNotFound(key, context): + return "Could not find key \(key) in JSON: \(context)" + case let .valueNotFound(type, context): + return "Could not find type \(type) in JSON: \(context)" + case let .typeMismatch(type, context): + return "Type mismatch for type \(type) in JSON: \(context)" + case let .dataCorrupted(context): + return "Data found to be corrupted in JSON: \(context)" } } } diff --git a/SWDestinyTrades/Classes/Rest/Base/HttpClient.swift b/SWDestinyTrades/Classes/Rest/Base/HttpClient.swift index 2b09d398..2030af60 100644 --- a/SWDestinyTrades/Classes/Rest/Base/HttpClient.swift +++ b/SWDestinyTrades/Classes/Rest/Base/HttpClient.swift @@ -17,67 +17,38 @@ final class HttpClient: HttpClientProtocol { } extension HttpClient { + var logger: NetworkingLogger { return NetworkingLogger(level: .debug) } - typealias DecodingCompletionHandler = (Decodable?, APIError?) -> Void + func request(_ request: URLRequest, decode: T.Type) async throws -> T { + + logger.log(request: request) - private func decodingTask(with request: URLRequest, - decodingType: T.Type, - completionHandler completion: @escaping (T?, RequestError?) -> Void) -> URLSessionDataTask - { let requestDate = Date() - let task = session.dataTask(with: request) { [weak self] data, response, error in + + do { + let (data, response) = try await session.data(for: request) let responseTime = Date().timeIntervalSince(requestDate) guard let httpResponse = response as? HTTPURLResponse else { - completion(nil, self?.failureReason(HttpStatusCode.unknown, error)) - return - } - let statusCode = HttpStatusCode(fromRawValue: httpResponse.statusCode) - switch statusCode { - case .ok ... .permanentRedirect: - if let data = data { - do { - let model = try JSONDecoder().decode(decodingType, from: data) - self?.logger.log(response: response, data: data, time: responseTime) - completion(model, nil) - } catch { - completion(nil, RequestError(statusCode, reason: .jsonConversionFailure)) - } - } else { - completion(nil, RequestError(statusCode, reason: .invalidData)) - } - default: - completion(nil, RequestError(statusCode, reason: .responseUnsuccessful)) + throw APIError.invalidData } - } - return task - } - func request(_ request: URLRequest, decode: ((T) -> T)?, completion: @escaping (Result) -> Void) { - logger.log(request: request) + logger.log(response: response, data: data, time: responseTime) - let task = decodingTask(with: request, decodingType: T.self) { [weak self] json, error in - - DispatchQueue.main.async { - if let error = error { - self?.logger.logError(request: request, statusCode: error.statusCode.rawValue, error: error.reason) - completion(.failure(error.reason)) - return - } - if let json = json { - if let value = decode?(json) { - completion(.success(value)) - } else { - completion(.success(json)) - } - } + guard case 200 ... 300 = httpResponse.statusCode else { + throw APIError.responseUnsuccessful } + + return try decodeData(data, decode: T.self) + } catch let error as URLError where error.code == .cancelled { + throw APIError.requestCancelled + } catch { + throw error } - task.resume() } func cancelAllRequests() { @@ -90,12 +61,19 @@ extension HttpClient { // MARK: - Helper - private func failureReason(_ statusCode: HttpStatusCode, _ error: Error?) -> RequestError { - var reason: APIError = .requestFailed(reason: error?.localizedDescription) - - if let error = error as NSError?, error.code == NSURLErrorCancelled { - reason = .requestCancelled + private func decodeData(_ data: Data, decode: T.Type) throws -> T { + do { + return try JSONDecoder().decode(T.self, from: data) + } catch let DecodingError.keyNotFound(key, context) { + throw APIError.keyNotFound(key: key, context: context.debugDescription) + } catch let DecodingError.valueNotFound(type, context) { + throw APIError.valueNotFound(type: type, context: context.debugDescription) + } catch let DecodingError.typeMismatch(type, context) { + throw APIError.typeMismatch(type: type, context: context.debugDescription) + } catch let DecodingError.dataCorrupted(context) { + throw APIError.dataCorrupted(context: context.debugDescription) + } catch let error as NSError { + throw APIError.jsonConversionFailure(domain: error.domain, description: error.localizedDescription) } - return RequestError(statusCode, reason: reason) } } diff --git a/SWDestinyTrades/Classes/Rest/Base/HttpClientProtocol.swift b/SWDestinyTrades/Classes/Rest/Base/HttpClientProtocol.swift index edee84c8..cdea45b0 100644 --- a/SWDestinyTrades/Classes/Rest/Base/HttpClientProtocol.swift +++ b/SWDestinyTrades/Classes/Rest/Base/HttpClientProtocol.swift @@ -11,13 +11,14 @@ import Foundation protocol HttpClientProtocol { var logger: NetworkingLogger { get } - func request(_ request: URLRequest, decode: ((T) -> T)?, completion: @escaping (Result) -> Void) + func request(_ request: URLRequest, decode: T.Type) async throws -> T func cancelAllRequests() } extension HttpClientProtocol { - func request(_ request: URLRequest, decode: ((T) -> T)? = nil, completion: @escaping (Result) -> Void) { - self.request(request, decode: decode, completion: completion) + + func request(_ request: URLRequest, decode: T.Type) async throws -> T { + try await self.request(request, decode: decode) } } diff --git a/SWDestinyTrades/Classes/Rest/SWDestinyService.swift b/SWDestinyTrades/Classes/Rest/SWDestinyService.swift index 8ebce9b9..1d66c76a 100644 --- a/SWDestinyTrades/Classes/Rest/SWDestinyService.swift +++ b/SWDestinyTrades/Classes/Rest/SWDestinyService.swift @@ -9,45 +9,46 @@ import Foundation final class SWDestinyService: SWDestinyServiceProtocol { + private let client: HttpClientProtocol init(client: HttpClientProtocol = HttpClient()) { self.client = client } - func search(query: String, completion: @escaping (Result<[CardDTO], APIError>) -> Void) { + func search(query: String) async throws -> [CardDTO] { let endpoint: SWDestinyEndpoint = .search(query: query) let request = endpoint.request - client.request(request, completion: completion) + return try await client.request(request, decode: [CardDTO].self) } - func retrieveSetList(completion: @escaping (Result<[SetDTO], APIError>) -> Void) { + func retrieveSetList() async throws -> [SetDTO] { let endpoint: SWDestinyEndpoint = .setList let request = endpoint.request - client.request(request, completion: completion) + return try await client.request(request, decode: [SetDTO].self) } - func retrieveSetCardList(setCode: String, completion: @escaping (Result<[CardDTO], APIError>) -> Void) { + func retrieveSetCardList(setCode: String) async throws -> [CardDTO] { let endpoint: SWDestinyEndpoint = .cardList(setCode: setCode) let request = endpoint.request - client.request(request, completion: completion) + return try await client.request(request, decode: [CardDTO].self) } - func retrieveAllCards(completion: @escaping (Result<[CardDTO], APIError>) -> Void) { + func retrieveAllCards() async throws -> [CardDTO] { let endpoint: SWDestinyEndpoint = .allCards let request = endpoint.request - client.request(request, completion: completion) + return try await client.request(request, decode: [CardDTO].self) } - func retrieveCard(cardId: String, completion: @escaping (Result) -> Void) { + func retrieveCard(cardId: String) async throws -> CardDTO { let endpoint: SWDestinyEndpoint = .card(cardId: cardId) let request = endpoint.request - client.request(request, completion: completion) + return try await client.request(request, decode: CardDTO.self) } func cancelAllRequests() { diff --git a/SWDestinyTrades/Classes/Rest/SWDestinyServiceProtocol.swift b/SWDestinyTrades/Classes/Rest/SWDestinyServiceProtocol.swift index fae1b083..376e0dab 100644 --- a/SWDestinyTrades/Classes/Rest/SWDestinyServiceProtocol.swift +++ b/SWDestinyTrades/Classes/Rest/SWDestinyServiceProtocol.swift @@ -9,15 +9,16 @@ import Foundation protocol SWDestinyServiceProtocol { - func search(query: String, completion: @escaping (Result<[CardDTO], APIError>) -> Void) - func retrieveSetList(completion: @escaping (Result<[SetDTO], APIError>) -> Void) + func search(query: String) async throws -> [CardDTO] - func retrieveSetCardList(setCode: String, completion: @escaping (Result<[CardDTO], APIError>) -> Void) + func retrieveSetList() async throws -> [SetDTO] - func retrieveAllCards(completion: @escaping (Result<[CardDTO], APIError>) -> Void) + func retrieveSetCardList(setCode: String) async throws -> [CardDTO] - func retrieveCard(cardId: String, completion: @escaping (Result) -> Void) + func retrieveAllCards() async throws -> [CardDTO] + + func retrieveCard(cardId: String) async throws -> CardDTO func cancelAllRequests() } diff --git a/SWDestinyTrades/Classes/Search/Controller/SearchListViewController.swift b/SWDestinyTrades/Classes/Search/Controller/SearchListViewController.swift index c7250da7..f5e345f5 100644 --- a/SWDestinyTrades/Classes/Search/Controller/SearchListViewController.swift +++ b/SWDestinyTrades/Classes/Search/Controller/SearchListViewController.swift @@ -61,15 +61,20 @@ final class SearchListViewController: UIViewController { navigationItem.title = L10n.search } - func search(query: String) { + private func search(query: String) { searchView.activityIndicator.startAnimating() - destinyService.search(query: query) { [weak self] result in - self?.searchView.activityIndicator.stopAnimating() - switch result { - case let .success(allCards): - self?.searchView.searchTableView.updateSearchList(allCards) - self?.cards = allCards - case let .failure(error): + Task { [weak self] in + guard let self else { return } + + defer { + self.searchView.activityIndicator.stopAnimating() + } + + do { + let allCards = try await self.destinyService.search(query: query) + self.searchView.searchTableView.updateSearchList(allCards) + self.cards = allCards + } catch { ToastMessages.showNetworkErrorMessage() LoggerManager.shared.log(event: .allCards, parameters: ["error": error.localizedDescription]) } diff --git a/SWDestinyTrades/Classes/Sets/Controller/SetsListViewController.swift b/SWDestinyTrades/Classes/Sets/Controller/SetsListViewController.swift index 5fdb1761..4f96b184 100644 --- a/SWDestinyTrades/Classes/Sets/Controller/SetsListViewController.swift +++ b/SWDestinyTrades/Classes/Sets/Controller/SetsListViewController.swift @@ -61,13 +61,18 @@ final class SetsListViewController: UIViewController { @objc func retrieveSets(sender: UIRefreshControl) { - destinyService.retrieveSetList { [weak self] result in - self?.setsView.endRefreshControl() - self?.setsView.activityIndicator.stopAnimating() - switch result { - case let .success(setList): - self?.setsView.setsTableView.updateSetList(setList) - case let .failure(error): + Task { [weak self] in + guard let self else { return } + + defer { + self.setsView.activityIndicator.stopAnimating() + self.setsView.endRefreshControl() + } + + do { + let setList = try await self.destinyService.retrieveSetList() + self.setsView.setsTableView.updateSetList(setList) + } catch { ToastMessages.showNetworkErrorMessage() LoggerManager.shared.log(event: .setsList, parameters: ["error": error.localizedDescription]) } diff --git a/Tuist/ProjectDescriptionHelpers/Target.swift b/Tuist/ProjectDescriptionHelpers/Target.swift index aab51380..df7b3b56 100644 --- a/Tuist/ProjectDescriptionHelpers/Target.swift +++ b/Tuist/ProjectDescriptionHelpers/Target.swift @@ -14,7 +14,7 @@ public extension Project { platform: .iOS, product: .app, bundleId: "br.com.anykey.SWDestiny-Trades", - deploymentTarget: .iOS(targetVersion: "12.0", devices: [.iphone, .ipad]), + deploymentTarget: .iOS(targetVersion: "13.0", devices: [.iphone, .ipad]), infoPlist: "SWDestinyTrades/Info.plist", sources: ["SWDestinyTrades/Classes/**"], resources: [