Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift. Combine. Is there any way to call a publisher block more than once when retry?

Tags:

swift

combine

I want to make a network request more than one time when some error occurs using retry() from Swift/Combine. The block inside the publisher is called once only which means one only one request is made for a real app when error happens. My code is:

import UIKit
import Combine
import PlaygroundSupport

enum TestFailureCondition: Error {
    case invalidServerResponse
}

var backgroundQueue: DispatchQueue = DispatchQueue(label: "backgroundQueue")

var failPublisher: AnyPublisher<(Data, URLResponse), Error> {
    Future<(Data, URLResponse), Error> { promise in
        print("Attempt to call")
        backgroundQueue.asyncAfter(deadline: .now() + Double.random(in: 1..<3)) {
            promise(.failure(TestFailureCondition.invalidServerResponse))
        }
    }
    .eraseToAnyPublisher()
}

let cancellable = failPublisher
.print("(1)>")
.retry(3)
.print("(2)>")
.sink(receiveCompletion: { fini in
    print(" ** .sink() received the completion:", String(describing: fini))


    PlaygroundPage.current.finishExecution()
}, receiveValue: { stringValue in
    print(" ** .sink() received \(stringValue)")
})

PlaygroundPage.current.needsIndefiniteExecution = true

I expect that backgroundQueue.asyncAfter(deadline) is called three time before some error happens. Does anyone know why?

like image 422
Dmitry Avatar asked Dec 07 '19 09:12

Dmitry


People also ask

How does combinelatest publisher work?

CombineLatest publisher collects the first value from all three publishers and emits them as a single tuple. CombineLatest continues sending new values even when only one publisher emits a new value.

Why should I use Combine in SwiftUI?

If you’ve been trying out SwiftUI, you’ve likely been using Combine quite a lot already. Types like ObservableObject and Property Wrappers like @Published all use Combine under the hood. It’s a powerful framework to dynamically respond to value changes over time. Build better software, faster.

What is the use of mergemany in Swift?

As you can see, the MergeMany operator allows me to create a single pipe for cached and fresh data where the cached information usually appears first and then replaced by new data. To learn about building custom Combine operators, take a look at my “Building custom Combine operators in Swift” post.

What are publishers and subscribers in the combine framework?

The Combine framework comes with so-called Publishers and subscribers. If you’re familiar with RxSwift: Different namings, but they both give the same understanding. A Publisher exposes values that can change on which a subscriber subscribes to receive all those updates.


2 Answers

Future runs its body once, as soon as it is created, even if nothing subscribes to it. It saves the result and completes all subscriptions with that same result. So using the retry operator on a Future won't make the Future run its body again on failure.

You want each subscription to run the body again. You can do that by wrapping your Future in a Deferred. The Deferred will create a new Future for every subscription.

var failPublisher: AnyPublisher<(Data, URLResponse), Error> {
    Deferred {
        Future<(Data, URLResponse), Error> { promise in
            print("Attempt to call")
            backgroundQueue.asyncAfter(deadline: .now() + Double.random(in: 1..<3)) {
                promise(.failure(TestFailureCondition.invalidServerResponse))
            }
        }
    }
    .eraseToAnyPublisher()
}
like image 73
rob mayoff Avatar answered Oct 07 '22 20:10

rob mayoff


I managed to achieve the expected behaviour by utilising tryCatch() function and making another request: Link

The link contains two ways to achieve the same behaviour including Deferred {} mentioned above.

like image 44
Dmitry Avatar answered Oct 07 '22 21:10

Dmitry