I'm working on a UITableView
that loads with preloaded data, then fetches additional items asynchronously. When I spam pull-to-refresh, each pull triggers a new async request, resulting in multiple callbacks and duplicate items.
Only the latest create()
call's async results are processed.
Previous async requests are ignored or canceled, avoiding duplicate items.
You asked:
To handle this, I need a way to cancel any previous pending requests each time pull-to-refresh is triggered, ensuring only the latest request is processed.
And in the comments you clarified:
I’m aiming for an abstraction layer where providers can supply items asynchronously, without needing to know or enforce how they’re doing it.
You provided a completion-handler-based example; an asynchronous API for AsyncProvider
that was merely loadItems(completion:)
. And more to the point, you did not specify any explicit cancellation mechanism for either AsyncProvider
in general, or for the specific call to loadItems
in particular. In short, you have not specified any mechanism to cancel the request.
If you survey the completion-handler-based asynchronous API, there are a variety of different cancellation patterns, including (but not limited to):
Cancel all requests.
CoreLocation offers a cancelGeocode
function, which simply cancels any geocode requests that might be in progress … but there is nothing to identify a particular geocode request.
Let the provider cancel a request.
The Photos framework’s PHImageManager
, each request returns its own identifier, and you then supply that identifier to the image manager’s cancelImageRequest
.
The provider returns an object that can be cancelled.
In URLSession
, functions like dataTask
return a URLSessionTask
, which you can cancel
on that task object, itself.
You create a cancellable object that is supplied to the provider.
CloudKit uses an Operation
-based pattern, where you create a CKFetchRecordsOperation
, and you can then use the cancel
interface offered by Operation
.
Bottom line, there is no single consistent pattern for canceling completion-handler-based asynchronous API (though most well-written API offer some sort of cancelation mechanism). Thus, we cannot offer a single pattern for canceling some generic completion-handler-based AsyncProvider
. Or, if you did (for example, a protocol modeled after the PHImageManager
pattern) you’d have to write some shim for those asynchronous API that adopt different patterns.
In Swift concurrency, however, there is a single, consistent mechanism for canceling asynchronous work. So, let us consider an async
-await
rendition of your AsyncProvider
:
final class AsyncProvider {
func loadItems() async throws -> [String] {
try await Task.sleep(for: .seconds(5))
return ["Async Item"]
}
}
Now the view controller (or what have you) the pattern would be:
class ViewController: UIViewController {
private var previousTask: Task<[String], Error>?
…
func reloadData() async throws {
// cancel prior task, if any, here
previousTask?.cancel()
// wrap `loadItems` in a `Task`
let task = Task {
try await provider.loadItems()
}
// save it
previousTask = task
// now await it (and handle cancellation, too)
try await withTaskCancellationHandler {
let items = try await task.value
// do something with items here
} onCancel: {
task.cancel()
}
}
}
The difference between the legacy completion-handler-based approaches and this Swift concurrency pattern is that the latter offers a consistent cancellation pattern.
Swift concurrency simplifies this “layer of abstraction”, because every Swift concurrency API should handle cancellation via the same mechanism, namely the cancellation of the task.
You said:
When calling
loadItems()
, it can potentially create a new instance with the same data each time. However, because each instance is technically unique, simply checking if the item already exists in the items array before adding it won’t work.
I might encourage you to investigate “diffable data sources”. See WWDC 2019 video Advances in UI Data Sources. It greatly simplifies the process and gracefully handles this “I’ve got an updated data model with a mix of unchanged data and updates; refresh the table for me” process.