0

I have iOS application which use remote API for receiving some data (transactons) by periods. API allows make one call per 10 seconds (otherwise my IP will be blocked). Because of network issues, returned HTTP error statuses (404, 500 etc) 10 seconds delay must be after complated API call. These errors must be handled and queue must be stopped. Also API can return predifined errors (like wrong authentication token, wrong dates etc). These errors also must be handled and queue with following API calls must be stopped.

Following code is simplified. I implemented what I need but I cannot handle first error if API is not reachable or returns predefined error. In this case runSyncTransactions always called twice because check self.lastApiError != nil is always TRUE such as lastApiError is always nill on first api call. And doesn't matter what kind of error I receive.

Thanks in advance.

import Foundation
import Combine

class ApiCaller: ObservableObject {
    internal var stopSync: Bool = false
    
    private let apiURLString: String = "https://api.google.com"
    private let delayBetweenApiRequestsInSec: Int = 10
    
    private var lastApiError: Error? = nil
    
    private var itemsSubscription: AnyCancellable?
        
    @Published var transactionModels: [TransactionModel] = []
    @Published var isSynchronizing: Bool = false
    
    static let shared = ApiCaller()
    
    private init() {}
    
    func syncAllScheduledAccounts() async throws {
        self.isSynchronizing = true
        print("Started sync")
        let dateNow = Date.now
        let dates: [Date] = [
            dateNow,
            Calendar.current.date(byAdding: .day, value: -30, to: dateNow)!,
            Calendar.current.date(byAdding: .day, value: -60, to: dateNow)!,
            Calendar.current.date(byAdding: .day, value: -90, to: dateNow)!,
        ]
        
        do {
            //            var outOfBoundsAccounts: [MoneyAccountEntity] = []
            for date in dates {
                // some code
                do {
                    try await runSyncTransactions(dateFrom: date, dateTo: Calendar.current.date(byAdding: .day, value: -29, to: dateNow)!)
                    // some code
                    //                } catch SomeError.valueFieldToOutOfBounds {
                    // some code
                    // continue
                } catch {
                    throw error
                }
                // some code
                if date != dates.last {
                    try await Task.sleep(nanoseconds: UInt64(delayBetweenApiRequestsInSec) * 1_000_000_000)
                }
            }
        } catch {
            self.isSynchronizing = false
            throw error
        }

        self.isSynchronizing = false
    }
    
    private func runSyncTransactions(dateFrom: Date, dateTo: Date) async throws {
        if(stopSync) {
            stopSync = false
            throw SomeError.synchronizationStoppedByUser
        }

        let dateFromTimestamp: Int = Int(dateFrom.timeIntervalSince1970)
        let dateToTimestamp: Int = Int(dateTo.timeIntervalSince1970)
        
        let requestUrlString: String = "\(apiURLString)/\(dateFromTimestamp)/\(dateToTimestamp)"
        
        guard let url: URL = URL(string: requestUrlString)
            else {
    //            throw "Bad URL: \(requestUrlString)"
            throw SomeError.error1
        }
        
        var request = URLRequest(url: url)
            request.httpMethod = "GET"

        itemsSubscription = URLSession.shared.dataTaskPublisher(for: request)
            .subscribe(on: DispatchQueue.global(qos: .default))
            .tryMap { (output) in
                guard let response = output.response as? HTTPURLResponse,
                      response.statusCode >= 200 && response.statusCode < 300 else {
                    // here handling of predefined API errors
                    let error = try? JSONDecoder().decode(ApiErrorResponse.self, from: output.data)
                    print("Error receiving transactions: \(error?.errorDescription ?? "")")
                    self.lastApiError = error != nil ? ApiError.makeError(errorResponse: error!) : SomeError.undefinedError
                    throw self.lastApiError!
                }
                return output.data
            }
            .receive(on: DispatchQueue.main)
            .decode(type: [TransactionItem].self, decoder: JSONDecoder())
            .sink ( receiveCompletion: { (completion) in
                    switch completion {
                    case .failure(let error):
                        print("(sink) Error sync transactions: \(error.localizedDescription)")
                        self.lastApiError = error
    //                    throw error
                    case .finished:
                        break
                    }
                }, receiveValue: { [weak self] (returnedItems) in
                    self?.itemsSubscription?.cancel()
                    
                    var transactionModelsTemp: [TransactionModel] = []
                    
                    print("Received transactions: \(returnedItems.count)")
                    
                    for item in returnedItems {
                        let transactionModel = TransactionConvertor.transactionItemToTransactonModel(item: item)
                        transactionModelsTemp.append(transactionModel)
                    }
                    
                    // this is @Published var and this data will be handlad and saved in other place
                    self?.transactionModels = transactionModelsTemp
                    self?.lastApiError = nil
                }
            )

        print("After Task.sleep")
        print(self.lastApiError as Any)
        // !!!!!!!!!! here is the problem !!!!!!!!!!
        // for first date in case of error self.lastApiError is always nil!
        if(self.lastApiError != nil) {
            throw self.lastApiError!
        }
    }
}

class TransactionConvertor {
    static func transactionItemToTransactonModel(item: TransactionItem) -> TransactionModel {
        return TransactionModel(
            id: UUID(),
            originalId: item.id,
            title: item.description,
            date: Date(timeIntervalSince1970: Double(item.time))
        )
    }
}

struct TransactionItem: Codable, Identifiable {
    let id: String
    let time: Int
    let description: String

    enum CodingKeys: String, CodingKey {
        case id, time, description
    }
}

struct TransactionModel: Identifiable, Codable, Hashable {
    let id: UUID
    var originalId, title: String
    var date: Date
    
    enum CodingKeys: String, CodingKey {
        case id, originalId, title, date
    }
}

struct ApiErrorResponse: Decodable, Error {
    let errorDescription: String
}

enum ApiError: Error {
    case tooManyRequests
    case invalidAccount
    case failedRequest
}

extension ApiError: LocalizedError {
    static func makeError(errorResponse: ApiErrorResponse) -> Self {
        switch errorResponse.errorDescription {
        case "Too many requests":
            return .tooManyRequests
        case "invalid account":
            return .invalidAccount
        default:
            return .failedRequest
        }
    }
    
    public var errorDescription: String? {
        switch self {
        case .tooManyRequests:
            return NSLocalizedString("Too many requests", comment: "")
        case .invalidAccount:
            return NSLocalizedString("Invalid Account", comment: "")
        case .failedRequest:
            return NSLocalizedString("Failed request", comment: "")
        }
    }
}

enum SomeError: Error {
    case error1
    case undefinedError
    case synchronizationStoppedByUser
}
2
  • An "error" reported from the API is not an error in the sense that URLSession understands it. Commented Oct 13, 2023 at 15:30
  • @matt I know. It's returned json data with error description. I handle this json and make appropriated Error. Commented Oct 13, 2023 at 17:31

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.