There is a function .retry() in Combine that helps to retry a request when something goes wrong with a long runing task. Although it is not enough just to call retry()
to achieve retrying logic. The retry() function does not do another request but it re-subscribes only to a publisher. The below approaches might be used to solve the problem.
To make another request the tryCatch() might be used. In the code below if the first call fails there are three attempts to retry (retry(3)) that are made:
import UIKit
import Combine
import PlaygroundSupport
enum CustomNetworkingError: Error {
case invalidServerResponse
}
let backgroundQueue: DispatchQueue = DispatchQueue(label: "backgroundQueue")
let backendURL = URL(string: "https://google1.com")!
func dataPublisher(for request: URLRequest) -> AnyPublisher<(data: Data, response: URLResponse), URLError> {
Future<(data: Data, response: URLResponse), URLError> { promise in
print("Make a request")
backgroundQueue.asyncAfter(deadline: .now() + 1) {
promise(.failure(URLError(.notConnectedToInternet)))
}
}
.eraseToAnyPublisher()
}
func dataLoader(backendURL: URL) -> AnyPublisher<Data, Error> {
let request = URLRequest(url: backendURL)
print("DataLoader")
return dataPublisher(for: request)
// We get here when a request fails
.tryCatch { (error) -> AnyPublisher<(data: Data, response: URLResponse), URLError> in
print("Try to handle an error")
guard error.code == .notConnectedToInternet else {
throw error
}
print("Re-try a request")
return dataPublisher(for: request) // <-- This is a point where another request is made
}
.tryMap { data, response -> Data in
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw CustomNetworkingError.invalidServerResponse
}
return data
}
.eraseToAnyPublisher()
}
let anyCancellable = dataLoader(backendURL: backendURL)
.retry(3) // <-- Literal constant regulates how many times we will end up in tryCatch() when a request fails
.subscribe(on: backgroundQueue)
.receive(on: RunLoop.main)
.sink(receiveCompletion: { (result) in
switch result {
case .finished: ()
case .failure(let error):
guard let error = error as? URLError else {
return
}
print(error)
}
PlaygroundPage.current.finishExecution()
}) { (data) in
print(data)
}
PlaygroundPage.current.needsIndefiniteExecution = true
The output of this code follows below:
DataLoader
Make a request
Try to handle an error
Re-try a request
Make a request
Try to handle an error (first attempt to retry)
Re-try a request
Make a request
Try to handle an error (second attempt to retry)
Re-try a request
Make a request
Try to handle an error (third attempt to retry)
Re-try a request
Make a request
URLError(_nsError: Error Domain=NSURLErrorDomain Code=-1009 "(null)")
func dataPublisher(for request: URLRequest) -> AnyPublisher<(data: Data, response: URLResponse), URLError> {
Deferred {
Future<(data: Data, response: URLResponse), URLError> { promise in
print("Make a request")
backgroundQueue.asyncAfter(deadline: .now() + 1) {
promise(.failure(URLError(.notConnectedToInternet)))
}
}
}
.eraseToAnyPublisher()
}
In this case there is no need to use tryCatch {}
. The downside of this approach is that there is no way to control whether we need to make another try or not. In this case it means three retries will be made regardless of the return value.
DataLoader
Make a request
Make a request
Make a request
Make a request
URLError(_nsError: Error Domain=NSURLErrorDomain Code=-1009 "(null)")
I couldn't understand the downside for the second approach: "... The downside of this approach is that there is no way to control whether we need to make another try or not...."
can you elaborate more ?
I can use any kind of operators with the publisher returned from deferred.