From fc26dfba4ffdccf6b1e9ab5500b7046802f73181 Mon Sep 17 00:00:00 2001 From: Mehran Kamalifard Date: Mon, 19 Aug 2024 14:02:11 +0900 Subject: [PATCH] Improved Error Handling Core api --- EasyCrypto.xcodeproj/project.pbxproj | 4 ++ .../Networking/Client/NetworkClient.swift | 38 +++++++++++-------- .../Networking/Utility/APIErrorResponse.swift | 24 ++++++++++++ .../CoinDetail/View/CoinDetailView.swift | 8 ++++ .../ViewModel/CoinDetailViewModel.swift | 5 +-- .../Presentation/Main/View/MainView.swift | 2 +- 6 files changed, 61 insertions(+), 20 deletions(-) create mode 100644 EasyCrypto/Core/Networking/Utility/APIErrorResponse.swift diff --git a/EasyCrypto.xcodeproj/project.pbxproj b/EasyCrypto.xcodeproj/project.pbxproj index bb00f69..010fb7b 100644 --- a/EasyCrypto.xcodeproj/project.pbxproj +++ b/EasyCrypto.xcodeproj/project.pbxproj @@ -106,6 +106,7 @@ A81742E0297E593D00023B28 /* EasyCryptoUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81742DF297E593D00023B28 /* EasyCryptoUITestsLaunchTests.swift */; }; A82CB994298A7A5500699C22 /* Publisher + Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A82CB993298A7A5500699C22 /* Publisher + Extension.swift */; }; A82CB996298A7AB900699C22 /* URLRequest + Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A82CB995298A7AB900699C22 /* URLRequest + Extension.swift */; }; + A8450C932C72FC0F002A3D24 /* APIErrorResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8450C922C72FC0F002A3D24 /* APIErrorResponse.swift */; }; A851A20C29890B4D007F4CF9 /* NetworkClientProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A851A20A29890B4C007F4CF9 /* NetworkClientProtocol.swift */; }; A851A20D29890B4D007F4CF9 /* NetworkClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = A851A20B29890B4D007F4CF9 /* NetworkClient.swift */; }; A851A21029890B75007F4CF9 /* NetworkClientManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A851A20F29890B75007F4CF9 /* NetworkClientManager.swift */; }; @@ -253,6 +254,7 @@ A81742DF297E593D00023B28 /* EasyCryptoUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EasyCryptoUITestsLaunchTests.swift; sourceTree = ""; }; A82CB993298A7A5500699C22 /* Publisher + Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Publisher + Extension.swift"; sourceTree = ""; }; A82CB995298A7AB900699C22 /* URLRequest + Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLRequest + Extension.swift"; sourceTree = ""; }; + A8450C922C72FC0F002A3D24 /* APIErrorResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIErrorResponse.swift; sourceTree = ""; }; A851A20A29890B4C007F4CF9 /* NetworkClientProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkClientProtocol.swift; sourceTree = ""; }; A851A20B29890B4D007F4CF9 /* NetworkClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkClient.swift; sourceTree = ""; }; A851A20F29890B75007F4CF9 /* NetworkClientManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkClientManager.swift; sourceTree = ""; }; @@ -978,6 +980,7 @@ A851A22629890C0A007F4CF9 /* APIError.swift */, A851A22729890C0B007F4CF9 /* APIDebuger.swift */, A851A22529890C0A007F4CF9 /* Configuration.swift */, + A8450C922C72FC0F002A3D24 /* APIErrorResponse.swift */, ); path = Utility; sourceTree = ""; @@ -1224,6 +1227,7 @@ A851A21529890B9F007F4CF9 /* NetworkTarget + Default.swift in Sources */, 02F1EEF429CDAE0E003AD8A9 /* CoreDataManager.swift in Sources */, 02D986142996AA320070A7E0 /* SearchMarketRepository.swift in Sources */, + A8450C932C72FC0F002A3D24 /* APIErrorResponse.swift in Sources */, A851A21429890B9F007F4CF9 /* NetworkTarget.swift in Sources */, A851A20D29890B4D007F4CF9 /* NetworkClient.swift in Sources */, 459B45D12A0BBCD3001C93BA /* Double + Extension.swift in Sources */, diff --git a/EasyCrypto/Core/Networking/Client/NetworkClient.swift b/EasyCrypto/Core/Networking/Client/NetworkClient.swift index 06a9d44..bae75a5 100644 --- a/EasyCrypto/Core/Networking/Client/NetworkClient.swift +++ b/EasyCrypto/Core/Networking/Client/NetworkClient.swift @@ -16,11 +16,15 @@ final class NetworkClient: NetworkClientProtocol { /// - session: The URLSession to use. Default: `URLSession.shared`. /// - logging: The logging utility to use. Default: `APIDebugger()`. /// - let session: URLSession + var session: URLSession { + let configuration = URLSessionConfiguration.default + configuration.waitsForConnectivity = true + return URLSession(configuration: configuration) + } + let logging: Logging - init(session: URLSession = .shared, logging: Logging = APIDebugger()) { - self.session = session + init(logging: Logging = APIDebugger()) { self.logging = logging } @@ -33,8 +37,21 @@ final class NetworkClient: NetworkClientProtocol { self.logging.logRequest(request: urlRequest) return publisher(request: urlRequest) .receive(on: scheduler) - .tryMap { result, _ -> Data in - return result + .tryMap { result, response -> Data in + guard let httpResponse = response as? HTTPURLResponse else { + throw APIError.invalidResponse(httpStatusCode: 0) + } + + if httpResponse.isResponseOK { + return result + } else { + // Attempt to decode error response + if let apiErrorResponse = try? decoder.decode(APIErrorResponse.self, from: result) { + throw APIError.statusMessage(message: apiErrorResponse.status.errorMessage) + } else { + throw APIError.invalidResponse(httpStatusCode: httpResponse.statusCode) + } + } } .decode(type: type.self, decoder: decoder) .mapError { error in @@ -52,17 +69,6 @@ final class NetworkClient: NetworkClientProtocol { .mapError { APIError.urlError($0) } .flatMap { response -> AnyPublisher<(data: Data, response: URLResponse), APIError> in self.logging.logResponse(response: response.response, data: response.data) - guard let httpResponse = response.response as? HTTPURLResponse else { - return Fail(error: APIError.invalidResponse(httpStatusCode: 0)) - .eraseToAnyPublisher() - } - - if !httpResponse.isResponseOK { - let error = NetworkClient.errorType(type: httpResponse.statusCode) - return Fail(error: error) - .eraseToAnyPublisher() - } - return Just(response) .setFailureType(to: APIError.self) .eraseToAnyPublisher() diff --git a/EasyCrypto/Core/Networking/Utility/APIErrorResponse.swift b/EasyCrypto/Core/Networking/Utility/APIErrorResponse.swift new file mode 100644 index 0000000..6cec8c1 --- /dev/null +++ b/EasyCrypto/Core/Networking/Utility/APIErrorResponse.swift @@ -0,0 +1,24 @@ +// +// APIErrorResponse.swift +// EasyCrypto +// +// Created by Mehran Kamalifard on 2024/08/19. +// + +import Foundation + +struct APIErrorResponse: Decodable { + + let status: Status +} + +struct Status: Decodable { + + let errorCode: Int + let errorMessage: String + + private enum CodingKeys: String, CodingKey { + case errorCode = "error_code" + case errorMessage = "error_message" + } +} diff --git a/EasyCrypto/Presentation/CoinDetail/View/CoinDetailView.swift b/EasyCrypto/Presentation/CoinDetail/View/CoinDetailView.swift index 9fd6949..bb57b17 100644 --- a/EasyCrypto/Presentation/CoinDetail/View/CoinDetailView.swift +++ b/EasyCrypto/Presentation/CoinDetail/View/CoinDetailView.swift @@ -65,6 +65,14 @@ struct CoinDetailView: Coordinatable { } .padding(.top) } + Spacer() + if presentAlert { + CustomAlertView(title: viewModel.errorTitle, + message: alertMessage , + primaryButtonLabel: "Ok") { + self.presentAlert = false + } + } } } .navigationBarTitle(String.empty) diff --git a/EasyCrypto/Presentation/CoinDetail/ViewModel/CoinDetailViewModel.swift b/EasyCrypto/Presentation/CoinDetail/ViewModel/CoinDetailViewModel.swift index 7e21af4..9776490 100644 --- a/EasyCrypto/Presentation/CoinDetail/ViewModel/CoinDetailViewModel.swift +++ b/EasyCrypto/Presentation/CoinDetail/ViewModel/CoinDetailViewModel.swift @@ -17,9 +17,10 @@ protocol DefaultCoinDetailViewModel: CoinDetailViewModelProtocol { } final class CoinDetailViewModel: DefaultViewModel, DefaultCoinDetailViewModel { private let coinDetailUsecase: CoinDetailUsecaseProtocol + + let errorTitle: String = Constants.Title.errorTitle @Published private(set) var coinData: CoinUnit? - @Published var isShowActivity: Bool = false var navigateSubject = PassthroughSubject() @@ -50,11 +51,9 @@ extension CoinDetailViewModel: DataFlowProtocol { func getCoinDetailData(id: String) { guard !id.isEmpty else { return } - isShowActivity = true call(argument: coinDetailUsecase.execute(id: id)) { [weak self] data in guard let data = data else { return } self?.coinData = data - self?.isShowActivity = false } } } diff --git a/EasyCrypto/Presentation/Main/View/MainView.swift b/EasyCrypto/Presentation/Main/View/MainView.swift index 9ff4e10..38fda7a 100644 --- a/EasyCrypto/Presentation/Main/View/MainView.swift +++ b/EasyCrypto/Presentation/Main/View/MainView.swift @@ -148,7 +148,7 @@ extension MainView { extension MainView { func showAlert(_ title: String, _ message: String) -> some View { CustomAlertView(title: title, message: message, primaryButtonLabel: "Retry", primaryButtonAction: { - self.presentAlert = true + self.presentAlert = false self.viewModel.callFirstTime() }) .previewLayout(.sizeThatFits)