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
}