How to restart dataTaskPublisher after failure? - ios

I have some code that makes a request to the net when the count value changes
#Published var count: Float = 0
init(data: SomeData) {
///
$count
.debounce(for: .seconds(1), scheduler: RunLoop.main)
.filter { return $0 != self.product.quantity }
.setFailureType(to: APIProviderError.self)
.flatMap { val -> AnyPublisher<Cart, APIProviderError> in
return self.cartService.update(item: params)
}
}
.sink { result in
print(result)
} receiveValue: { cart in
print(cart)
}
.store(in: &cancellable)
///
}
The cartService.update return dataTaskPublisher.
When any error is returned, the flatmap is never called again.
Can I restart it?

Can I restart it?
No. In the Combine framework, when a pipeline fails, the whole pipeline is cancelled and the publisher is done.
However, inside your flatMap you can build a mini-pipeline that does a catch and prevents the failure from escaping into the outer pipeline. You would then be able to keep using the outer pipeline if desired.
Also, if you think it will do any good, you can prevent the error from promulgating with a retry, which will try the fetch again. A data task publisher only fetches once, unless a retry makes it try again.

Related

CombineLatest operator is not emitting when inners publishers use subscribe(on:)

I'm observing an unexpected behavior regarding CombineLatest, if the inner publishers has subscribe(on:), the CombineLatest stream is not emitting any value.
Notes:
With Zip operator is working
Moving the subscribe(on:) / receive(on:) to the combineLatest stream also work. But in this particular use case, the inner publishers is defining their subscribe/receive
because are (re)used in other places.
Adding subscribe(on:)/receive(on:) to only one of the inner publishers also work, so the problem is just when both have it.
func makePublisher() -> AnyPublisher<Int, Never> {
Deferred {
Future { promise in
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 3) {
promise(.success(Int.random(in: 0...3)))
}
}
}
.subscribe(on: DispatchQueue.global())
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
var cancellables = Set<AnyCancellable>()
Publishers.CombineLatest(
makePublisher(),
makePublisher()
)
.sink { completion in
print(completion)
} receiveValue: { (a, b) in
print(a, b)
}.store(in: &cancellables)
Is this a combine bug or expected behavior? Do you have any idea of how can be setup this kind of stream where the inners can define its own subscribe scheduler?
Yes, it's a bug. We can simplify the test case to this:
import Combine
import Dispatch
let pub = Just("x")
.subscribe(on: DispatchQueue.main)
let ticket = pub.combineLatest(pub)
.sink(
receiveCompletion: { print($0) },
receiveValue: { print($0) })
This never prints anything. But if you comment out the subscribe(on:) operator, it prints what's expected. If you leave subscribe(on:) in, but insert some print() operators, you'll see that the CombineLatest operator never sends any demand upstream.
I suggest you copy the CombineX reimplementation of CombineLatest and the utilities it needs to compile (the CombineX implementations of Lock and LockedAtomic, I think). I don't know that the CombineX version works either, but if it's buggy, at least you have the source and can try to fix it.

iOS Combine Start new request only if previous has finished

I have network request that triggers every last cell in switui appearas. Sometimes if user scrolls fast enough down -> up -> request will trigger before first one finishes. Without combine or reactive approach I have completion block and bool value to handle this:
public func load() {
guard !isLoadingPosts else { return }
isLoadingPosts = true
postsDataProvider.loadMorePosts { _ in
self.isLoadingPosts = false
}
}
I was wondering if with combine this can be resolved more elegantly, without the need to use bool value. For example execute request only if previous has finished?
It looks like you want to skip making the call if it's already in progress.
Since you didn't share any of the Combine code you might have, I'll assume that you have a publisher-returning function like this:
func loadMorePosts() -> AnyPublisher<[Post], Error> {
//...
}
Then you can use a subject to initiate a load call, a flatMap(maxPublishers:_:) downstream, with a number of publishers limited to 1:
let loadSubject = PassthroughSubject<Void, Never>()
loadSubject
.flatMap(maxPublishers: .max(1)) {
loadMorePosts()
}
.sink(
receiveCompletion: { _ in },
receiveValue: { posts in
// update posts
})
.store(in: &cancellables)
The above pipeline subscribes to the subject, but if another value arrives before flatMap is ready to receive it, it would simply be dropped.
Then the load function becomes:
func load() {
loadSubject.send(())
}

Combine framework retry after delay?

I see how to use .retry directly, to resubscribe after an error, like this:
URLSession.shared.dataTaskPublisher(for:url)
.retry(3)
But that seems awfully simple-minded. What if I think that this error might go away if I wait awhile? I could insert a .delay operator, but then the delay operates even if there is no error. And there doesn't seem to be a way to apply an operator conditionally (i.e. only when there's an error).
I see how I could work around this by writing a RetryWithDelay operator from scratch, and indeed such an operator has been written by third parties. But is there a way to say "delay if there's an error", purely using the operators we're given?
My thought was that I could use .catch, because its function runs only if there is an error. But the function needs to return a publisher, and what publisher would we use? If we return somePublisher.delay(...) followed by .retry, we'd be applying .retry to the wrong publisher, wouldn't we?
It was a topic of conversation on the Using Combine project repo a while back - the whole thread: https://github.com/heckj/swiftui-notes/issues/164.
The long and short was we made an example that I think does what you want, although it does use catch:
let resultPublisher = upstreamPublisher.catch { error -> AnyPublisher<String, Error> in
return Publishers.Delay(upstream: upstreamPublisher,
interval: 3,
tolerance: 1,
scheduler: DispatchQueue.global())
// moving retry into this block reduces the number of duplicate requests
// In effect, there's the original request, and the `retry(2)` here will operate
// two additional retries on the otherwise one-shot publisher that is initiated with
// the `Publishers.Delay()` just above. Just starting this publisher with delay makes
// an additional request, so the total number of requests ends up being 4 (assuming all
// fail). However, no delay is introduced in this sequence if the original request
// is successful.
.retry(2)
.eraseToAnyPublisher()
}
This is referencing the a retry pattern I have in the book/online, which is basically what you describe (but wasn't what you asked about).
The person I was corresponding with on the issue provided a variant in that thread as an extension that might be interesting as well:
extension Publisher {
func retryWithDelay<T, E>()
-> Publishers.Catch<Self, AnyPublisher<T, E>> where T == Self.Output, E == Self.Failure
{
return self.catch { error -> AnyPublisher<T, E> in
return Publishers.Delay(
upstream: self,
interval: 3,
tolerance: 1,
scheduler: DispatchQueue.global()).retry(2).eraseToAnyPublisher()
}
}
}
I found a few quirks with the implementations in the accepted answer.
Firstly the first two attempts will be fired off without a delay since the first delay will only take effect after the second attempt.
Secondly if any one of the retry attempts succeed, the output value will also delayed which seems unnecessary.
Thirdly the extension is not flexible enough to allow the user to decide which scheduler it would like the retry attempts to be dispatched to.
After some tinkering back and forth I ended up with a solution like this:
public extension Publisher {
/**
Creates a new publisher which will upon failure retry the upstream publisher a provided number of times, with the provided delay between retry attempts.
If the upstream publisher succeeds the first time this is bypassed and proceeds as normal.
- Parameters:
- retries: The number of times to retry the upstream publisher.
- delay: Delay in seconds between retry attempts.
- scheduler: The scheduler to dispatch the delayed events.
- Returns: A new publisher which will retry the upstream publisher with a delay upon failure.
~~~
let url = URL(string: "https://api.myService.com")!
URLSession.shared.dataTaskPublisher(for: url)
.retryWithDelay(retries: 4, delay: 5, scheduler: DispatchQueue.global())
.sink { completion in
switch completion {
case .finished:
print("Success 😊")
case .failure(let error):
print("The last and final failure after retry attempts: \(error)")
}
} receiveValue: { output in
print("Received value: \(output)")
}
.store(in: &cancellables)
~~~
*/
func retryWithDelay<S>(
retries: Int,
delay: S.SchedulerTimeType.Stride,
scheduler: S
) -> AnyPublisher<Output, Failure> where S: Scheduler {
self
.delayIfFailure(for: delay, scheduler: scheduler)
.retry(retries)
.eraseToAnyPublisher()
}
private func delayIfFailure<S>(
for delay: S.SchedulerTimeType.Stride,
scheduler: S
) -> AnyPublisher<Output, Failure> where S: Scheduler {
self.catch { error in
Future { completion in
scheduler.schedule(after: scheduler.now.advanced(by: delay)) {
completion(.failure(error))
}
}
}
.eraseToAnyPublisher()
}
}
I remembered that the RxSwiftExt library had a really nice implementation of a custom retry + delay operator with many options (linear and exponential delay, plus an option to provide a custom closure) and I tried to recreate it in Combine. The original implementation is here.
/**
Provides the retry behavior that will be used - the number of retries and the delay between two subsequent retries.
- `.immediate`: It will immediatelly retry for the specified retry count
- `.delayed`: It will retry for the specified retry count, adding a fixed delay between each retry
- `.exponentialDelayed`: It will retry for the specified retry count.
The delay will be incremented by the provided multiplier after each iteration
(`multiplier = 0.5` corresponds to 50% increase in time between each retry)
- `.custom`: It will retry for the specified retry count. The delay will be calculated by the provided custom closure.
The closure's argument is the current retry
*/
enum RetryBehavior<S> where S: Scheduler {
case immediate(retries: UInt)
case delayed(retries: UInt, time: TimeInterval)
case exponentialDelayed(retries: UInt, initial: TimeInterval, multiplier: Double)
case custom(retries: UInt, delayCalculator: (UInt) -> TimeInterval)
}
fileprivate extension RetryBehavior {
func calculateConditions(_ currentRetry: UInt) -> (maxRetries: UInt, delay: S.SchedulerTimeType.Stride) {
switch self {
case let .immediate(retries):
// If immediate, returns 0.0 for delay
return (maxRetries: retries, delay: .zero)
case let .delayed(retries, time):
// Returns the fixed delay specified by the user
return (maxRetries: retries, delay: .seconds(time))
case let .exponentialDelayed(retries, initial, multiplier):
// If it is the first retry the initial delay is used, otherwise it is calculated
let delay = currentRetry == 1 ? initial : initial * pow(1 + multiplier, Double(currentRetry - 1))
return (maxRetries: retries, delay: .seconds(delay))
case let .custom(retries, delayCalculator):
// Calculates the delay with the custom calculator
return (maxRetries: retries, delay: .seconds(delayCalculator(currentRetry)))
}
}
}
public typealias RetryPredicate = (Error) -> Bool
extension Publisher {
/**
Retries the failed upstream publisher using the given retry behavior.
- parameter behavior: The retry behavior that will be used in case of an error.
- parameter shouldRetry: An optional custom closure which uses the downstream error to determine
if the publisher should retry.
- parameter tolerance: The allowed tolerance in firing delayed events.
- parameter scheduler: The scheduler that will be used for delaying the retry.
- parameter options: Options relevant to the scheduler’s behavior.
- returns: A publisher that attempts to recreate its subscription to a failed upstream publisher.
*/
func retry<S>(
_ behavior: RetryBehavior<S>,
shouldRetry: RetryPredicate? = nil,
tolerance: S.SchedulerTimeType.Stride? = nil,
scheduler: S,
options: S.SchedulerOptions? = nil
) -> AnyPublisher<Output, Failure> where S: Scheduler {
return retry(
1,
behavior: behavior,
shouldRetry: shouldRetry,
tolerance: tolerance,
scheduler: scheduler,
options: options
)
}
private func retry<S>(
_ currentAttempt: UInt,
behavior: RetryBehavior<S>,
shouldRetry: RetryPredicate? = nil,
tolerance: S.SchedulerTimeType.Stride? = nil,
scheduler: S,
options: S.SchedulerOptions? = nil
) -> AnyPublisher<Output, Failure> where S: Scheduler {
// This shouldn't happen, in case it does we finish immediately
guard currentAttempt > 0 else { return Empty<Output, Failure>().eraseToAnyPublisher() }
// Calculate the retry conditions
let conditions = behavior.calculateConditions(currentAttempt)
return self.catch { error -> AnyPublisher<Output, Failure> in
// If we exceed the maximum retries we return the error
guard currentAttempt <= conditions.maxRetries else {
return Fail(error: error).eraseToAnyPublisher()
}
if let shouldRetry = shouldRetry, shouldRetry(error) == false {
// If the shouldRetry predicate returns false we also return the error
return Fail(error: error).eraseToAnyPublisher()
}
guard conditions.delay != .zero else {
// If there is no delay, we retry immediately
return self.retry(
currentAttempt + 1,
behavior: behavior,
shouldRetry: shouldRetry,
tolerance: tolerance,
scheduler: scheduler,
options: options
)
.eraseToAnyPublisher()
}
// We retry after the specified delay
return Just(()).delay(for: conditions.delay, tolerance: tolerance, scheduler: scheduler, options: options).flatMap {
return self.retry(
currentAttempt + 1,
behavior: behavior,
shouldRetry: shouldRetry,
tolerance: tolerance,
scheduler: scheduler,
options: options
)
.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
}
Using .catch is indeed the answer. We simply make a reference to the data task publisher and use that reference as the head of both pipelines — the outer pipeline that does the initial networking, and the inner pipeline produced by the .catch function.
Let's start by creating the data task publisher and stop:
let pub = URLSession.shared.dataTaskPublisher(for: url).share()
Now I can form the head of the pipeline:
let head = pub.catch {_ in pub.delay(for: 3, scheduler: DispatchQueue.main)}
.retry(3)
That should do it! head is now a pipeline that inserts a delay operator only just in case there is an error. We can then proceed to form the rest of the pipeline, based on head.
Observe that we do indeed change publishers; if there is a failure and the catch function runs, the pub which is the upstream of the .delay becomes the publisher, replacing the pub we started out with. However, they are the same object (because I said share), so this is a distinction without a difference.

Convert URLSession.DataTaskPublisher to Future publisher

How to convert URLSession.DataTaskPublisher to Future in Combine framework.
In my opinion, the Future publisher is more appropriate here because the call can emit only one response and fails eventually.
In RxSwift there is helper method like asSingle.
I have achieved this transformation using the following approach but have no idea if this is the best method.
return Future<ResponseType, Error>.init { (observer) in
self.urlSession.dataTaskPublisher(for: urlRequest)
.tryMap { (object) -> Data in
//......
}
.receive(on: RunLoop.main)
.sink(receiveCompletion: { (completion) in
if case let .failure(error) = completion {
observer(.failure(error))
}
}) { (response) in
observer(.success(response))
}.store(in: &self.cancellable)
}
}
Is there any easy way to do this?
As I understand it, the reason to use .asSingle in RxSwift is that, when you subscribe, your subscriber receives a SingleEvent which is either a .success(value) or a .error(error). So your subscriber doesn't have to worry about receiving a .completion type of event, because there isn't one.
There is no equivalent to that in Combine. In Combine, from the subscriber's point of view, Future is just another sort of Publisher which can emit output values and a .finished or a .failure(error). The type system doesn't enforce the fact that a Future never emits a .finished.
Because of this, there's no programmatic reason to return a Future specifically. You could argue that returning a Future documents your intent to always return either exactly one output, or a failure. But it doesn't change the way you write your subscriber.
Furthermore, because of Combine's heavy use of generics, as soon as you want to apply any operator to a Future, you don't have a future anymore. If you apply map to some Future<V, E>, you get a Map<Future<V, E>, V2>, and similar for every other operator. The types quickly get out of hand and obscure the fact that there's a Future at the bottom.
If you really want to, you can implement your own operator to convert any Publisher to a Future. But you'll have to decide what to do if the upstream emits .finished, since a Future cannot emit .finished.
extension Publisher {
func asFuture() -> Future<Output, Failure> {
return Future { promise in
var ticket: AnyCancellable? = nil
ticket = self.sink(
receiveCompletion: {
ticket?.cancel()
ticket = nil
switch $0 {
case .failure(let error):
promise(.failure(error))
case .finished:
// WHAT DO WE DO HERE???
fatalError()
}
},
receiveValue: {
ticket?.cancel()
ticket = nil
promise(.success($0))
})
}
}
}
Instead of converting a data task publisher into a Future, convert a data task into a Future. Just wrap a Future around a call to a URLSession's dataTask(...){...}.resume() and the problem is solved. This is exactly what a future is for: to turn any asynchronous operation into a publisher.
How to return a future from a function
Instead of trying to return a 'future' from a function you need to convert your existing publisher to a AnyPublisher<Value, Error>. You do this by using the .eraseToAnyPublisher() operator.
func getUser() -> AnyPublisher<User, Error> {
URLSession.shared.dataTaskPublisher(for: request)
.tryMap { output -> Data in
// handle response
return output.data
}
.decode(type: User.self, decoder: JSONDecoder())
.mapError { error in
// handle error
return error
}
.eraseToAnyPublisher()
}

How to schedule a synchronous sequence of asynchronous calls in Combine?

I'd like to handle a series of network calls in my app. Each call is asynchronous and flatMap() seems like the right call. However, flatMap processes all arguments at the same time and I need the calls to be sequential -- the next network call starts only after the previous one is finished. I looked up an RxSwift answer but it requires concatMap operator that Combine does not have. Here is rough outline of what I'm trying to do, but flatMap fires all myCalls at the same time.
Publishers.Sequence(sequence: urls)
.flatMap { url in
Publishers.Future<Result, Error> { callback in
myCall { data, error in
if let data = data {
callback(.success(data))
} else if let error = error {
callback(.failure(error))
}
}
}
}
After experimenting for a while in a playground, I believe I found a solution, but if you have a better idea, please share. The solution is to add maxPublishers parameter to flatMap and set the value to max(1)
Publishers.Sequence(sequence: urls)
.flatMap(maxPublishers: .max(1)) // <<<<--- here
{ url in
Publishers.Future<Result, Error> { callback in
myCall { data, error in
if let data = data {
callback(.success(data))
} else if let error = error {
callback(.failure(error))
}
}
}
}
You can also use prepend(_:) method on observable which creates concatenated sequence which, I suppose is similar to Observable.concat(:) in RxSwift.
Here is a simple example that I tried to simulate your use case, where I have few different sequences which are followed by one another.
func dataTaskPublisher(_ urlString: String) -> AnyPublisher<(data: Data, response: URLResponse), Never> {
let interceptedError = (Data(), URLResponse())
return Publishers.Just(URL(string: urlString)!)
.flatMap {
URLSession.shared
.dataTaskPublisher(for: $0)
.replaceError(with: interceptedError)
}
.eraseToAnyPublisher()
}
let publisher: AnyPublisher<(data: Data, response: URLResponse), Never> = Publishers.Empty().eraseToAnyPublisher()
for urlString in [
"http://ipv4.download.thinkbroadband.com/1MB.zip",
"http://ipv4.download.thinkbroadband.com/50MB.zip",
"http://ipv4.download.thinkbroadband.com/10MB.zip"
] {
publisher = publisher.prepend(dataTaskPublisher(urlString)).eraseToAnyPublisher()
}
publisher.sink(receiveCompletion: { completion in
print("Completed")
}) { response in
print("Data: \(response)")
}
Here, prepend(_:) operator prefixes the sequence and so, prepended sequences starts first, completes and next sequence start.
If you run the code below, you should see that firstly 10 MB file is download, then 50 MB and at last 1 MB, since the last prepended starts first and so on.
There is other variant of prepend(_:) operator which takes array, but that does not seem to work sequentially.

Resources