This is Understanding Combine, written by Matt Neuburg. Corrections and suggestions are greatly appreciated (you can comment here). So are donations; please consider keeping me going by funding this work at http://www.paypal.me/mattneub. Or buy my books: the current (and final) editions are iOS 15 Programming Fundamentals with Swift and Programming iOS 14. Thank you!
.retry
(Publishers.Retry) takes an Int parameter, which it stores. If a failure comes down the pipeline from upstream, this operator does not pass the failure on downstream; instead, it decrements the Int, and then unsubscribes itself and subscribes itself to the upstream object, again. This causes the upstream publisher to start over publishing again. This can go on for as long as the original Int permits. If the Int reaches zero and a failure comes down the pipeline from upstream, now this operator sends the failure on downstream.
The classic example of using .retry
is with a data task publisher:
URLSession.shared.dataTaskPublisher(for:url)
.retry(3)
Unfortunately, the .retry
operator gives you no way to specify a delay before retrying, so if you think the problem might clear up if you wait a while, you have to insert a .delay
operator:
let pub = URLSession.shared.dataTaskPublisher(for: url)
.delay(for: 3, scheduler: DispatchQueue.main)
.retry(3)
But that’s not an ideal solution, because the .delay
runs regardless of whether there’s an error or not. What we’d like to do is insert the .delay
operator only if there was a failure.
That problem can be solved through the use of .catch
. The .catch
operator’s function runs only if there is an error from upstream. When we return a publisher from that function, it replaces the upstream publisher and becomes the new publisher. But what publisher should we return?
Well, we could just make another data task publisher:
URLSession.shared.dataTaskPublisher(for: url)
.catch { _ in
URLSession.shared.dataTaskPublisher(for: url)
.delay(for: 3, scheduler: DispatchQueue.main)
}.retry(3)
However, that’s messy, and it disobeys the .retry
count: we connect and fail, we substitute the new data task publisher, we connect and fail again, and now we start retrying, so that if we keep failing we end up with five connections instead of four. We can fix that by making a reference to the initial data task publisher with share()
applied to it:
let pub = URLSession.shared.dataTaskPublisher(for: url).share()
Now we can use pub
in two places: as the upstream of the .catch
operator, and as the upstream of the .delay
operator inside the .catch
function:
let pub = URLSession.shared.dataTaskPublisher(for: url).share()
let head = pub.catch {_ in
pub.delay(for: 3, scheduler: DispatchQueue.main)
}.retry(3)
That works correctly. If the initial data task publisher fails, the catch
function runs and returns a new publisher, namely the very same data task publisher with a .delay
attached to it.