I'm converting our app's networking to use async/await, and I'm having a little trouble coming up with a satisfactory pattern to replace the completion handler that we were using previously. Here is some pseudo-code of what we do:
func loadData() {
fetchData {
showTheSearchField()
}
}
func fetchData(completion: () -> ()) {
doTheActualNetworking()
if networkingSucceeded {
completion()
} else {
putUpAnAlertWithATryAgainButton(buttonAction: {
fetchData(completion: completion)
}
}
}
You see the point here. First we try to fetch the data, which we do by performing actual networking. Then:
If we succeeded, and only if we succeeded, then do whatever the caller wants done in case of success (what I have called
completion, but perhaps a better name would beonSuccess).If we failed, put up an alert with a try again button, and the action of that button is to fetch the data again and behave just the same way again — if we succeed this time, and only if we succeed this time, then do whatever the caller wants done in case of success.
My problem is that when I convert to async await, I can't find a way to eliminate the blasted the completion handler. For instance, in my example the completion handler says to show the search field. I can't just wrap the whole thing up as e.g. a throwing async method:
func loadData() {
Task {
try await fetchData()
showTheSearchField()
}
}
func fetchData() async throws {
do {
try await doTheActualNetworking()
} catch {
putUpAnAlertWithATryAgainButton(buttonAction: {
Task {
try await fetchData()
}
}
throw error
}
}
You see what's wrong with that? If we succeed on doTheActualNetworking the first time, fine, we fall back into the task in loadData and we proceed to show the search field. But if we fail and put up the alert and the user taps the try again button and we succeed this time, we won't show the search field, because we failed and threw and loadData came to an end.
So it seems to me that I am forced, because of the cyclic nature of the try again button, to maintain the completion / success handler, passing it into fetchData so that the try again button can recurse with it, just as I was doing before.
Well, that works fine, of course, but it feels to me like an anti-pattern. I'm adopting async/await exactly because I'd like never to see another completion handler as long as I live. Am I just stuck with the completion handler, or is there a way around this that I'm not seeing?
One approach would be to follow your recursive pattern:
Or, obviously, you could also do it non-recursively (perhaps adding some “max retry” logic, too; that’s up to you):
FWIW, to wrap the alert in a continuation in UIKit, for example, it might look like:
Or, just wrap your current completion-handler-based function in a continuation:
Personally, I would be inclined to give the “retry” UI a “cancel” option (so the user isn’t stuck in an endless loop if they happen to be in a no-network scenario) and then give the completion-handler a parameter indicating the result of which button they tapped. And in that case, we would then use a throwing continuation, like the
UIAlertControllerexample, but it comes down to whether the app is really completely dependent upon the presence of a functioning network connection or not. Obviously, if there is no “cancel” option, it would be a non-throwing continuation, and you would eliminate thetryin the calls, but the idea is the same.There are lots of variations on the theme, but hopefully it illustrates the idea.