Skip to content

Instantly share code, notes, and snippets.

@asam139
Forked from rdv0011/swift-combine-retry.md
Created February 6, 2020 20:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save asam139/e6576e177ed4dbd061d6fd4efcef68d1 to your computer and use it in GitHub Desktop.
Save asam139/e6576e177ed4dbd061d6fd4efcef68d1 to your computer and use it in GitHub Desktop.
Retry operation in Swift/Combine.

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.

Using tryCatch

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)")

Using Deferred {}

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)")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment