Skip to content

Commit

Permalink
Convert Network from Result+Completion to Async+Await
Browse files Browse the repository at this point in the history
  • Loading branch information
dogo committed Nov 23, 2023
1 parent 3230398 commit e374293
Show file tree
Hide file tree
Showing 17 changed files with 286 additions and 377 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -87,6 +76,23 @@ final class AddCardViewController: UIViewController {

// MARK: - Helpers

private func fetchAllCards() {
addCardView.startLoading()
Task { [weak self] in
guard let self else { return }

do {
let allCards = try await self.destinyService.retrieveAllCards()
self.addCardView.stopLoading()
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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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])
}
}
}
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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) {
Expand Down
18 changes: 15 additions & 3 deletions SWDestinyTrades/Classes/Rest/Base/APIError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)"
}
}
}
Expand Down
81 changes: 30 additions & 51 deletions SWDestinyTrades/Classes/Rest/Base/HttpClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,66 +17,38 @@ final class HttpClient: HttpClientProtocol {
}

extension HttpClient {

var logger: NetworkingLogger {
return NetworkingLogger(level: .debug)
}

typealias DecodingCompletionHandler = (Decodable?, APIError?) -> Void
func request<T: Decodable>(_ request: URLRequest, decode: T.Type) async throws -> T {

logger.log(request: request)

private func decodingTask<T: Decodable>(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<T: Decodable>(_ request: URLRequest, decode: ((T) -> T)?, completion: @escaping (Result<T, APIError>) -> 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() {
Expand All @@ -89,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<T: Decodable>(_ 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)
}
}
7 changes: 4 additions & 3 deletions SWDestinyTrades/Classes/Rest/Base/HttpClientProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ import Foundation
protocol HttpClientProtocol {
var logger: NetworkingLogger { get }

func request<T: Decodable>(_ request: URLRequest, decode: ((T) -> T)?, completion: @escaping (Result<T, APIError>) -> Void)
func request<T: Decodable>(_ request: URLRequest, decode: T.Type) async throws -> T

func cancelAllRequests()
}

extension HttpClientProtocol {
func request<T: Decodable>(_ request: URLRequest, decode: ((T) -> T)? = nil, completion: @escaping (Result<T, APIError>) -> Void) {
self.request(request, decode: decode, completion: completion)

func request<T: Decodable>(_ request: URLRequest, decode: T.Type) async throws -> T {
try await self.request(request, decode: decode)
}
}
21 changes: 11 additions & 10 deletions SWDestinyTrades/Classes/Rest/SWDestinyService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<CardDTO, APIError>) -> 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() {
Expand Down
11 changes: 6 additions & 5 deletions SWDestinyTrades/Classes/Rest/SWDestinyServiceProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<CardDTO, APIError>) -> Void)
func retrieveAllCards() async throws -> [CardDTO]

func retrieveCard(cardId: String) async throws -> CardDTO

func cancelAllRequests()
}
Loading

0 comments on commit e374293

Please sign in to comment.