So I have a paginated API that I have to call every time a user enters a screen. The below code works fine but has one issue if say API calls are in progress and the user backs down and comes again then rather both execution starts simultaneously.
and has to wait for both executions to be completed.
So, What I want is to stop ongoing API calls and start new ones when requested new one
If this setup is wrong in any way please notify me as well what am I doing wrong?
func storeAllData(matchID: Int64) {
GlobalVariable.isFetchingData = true
dbManager = DbManager.shared
if let lastScore = dbManager.getLastBallForCurrentMatch(matchID: matchID) {
self.fetchAllScore(scoreID: lastScore.id ?? 0, matchID: matchID)
} else {
self.fetchAllScore(scoreID: 0, matchID: matchID)
}
}
func fetchAllScore(scoreID: Int = 0, matchID: Int64) {
let id = matchID
backgroundQueue.async { [self] in
autoreleasepool {
fetchPaginatedData(matchID:Int(id), scoreID: scoreID, page: 1)
dispatchGroup.wait()
print("All API calls finished")
}
}
}
func fetchPaginatedData(matchID: Int,scoreID: Int = 0, page: Int) {
dispatchGroup.enter()
MatchesNetworkManager.sharedInstance.getAllScore(matchID: Int64(matchID), scoreID: Int64(scoreID), page: Int64(page), completionHandler: { [weak self] (allSocre, isSuccess, errorStr) in
if isSuccess {
if allSocre?.match?.scores != nil {
self?.oldScores.append(contentsOf: ((allSocre?.match?.scores)!))
self?.backgroundQueue.async {
if let scores = allSocre?.match?.scores as? [Scores] {
self?.dbManager.insertRecords(matchID: matchID, records: scores)
DispatchQueue.main.async {
self?.oldScores = []
if allSocre?.page_num ?? 0 == 1 {
self?.updateScoreView()
}
if (allSocre?.page_num ?? 0) < (allSocre?.total_pages ?? 0) {
self?.fetchPaginatedData(matchID: matchID,page: (allSocre?.page_num ?? 1) + 1)
} else {
// All pages have been fetched
GlobalVariable.isFetchingData = false
print("All API calls finished")
NotificationCenter.default.post(name: .refreshMarketView, object: nil, userInfo: nil)
self?.dispatchGroup.leave()
}
}
}
}
} else {
self?.updateScoreView()
GlobalVariable.isFetchingData = false
}
} else {
print("API call error: (errorStr)")
self?.updateScoreView()
GlobalVariable.isFetchingData = false
self?.dispatchGroup.leave()
}
})
}
2
Answers
The reality of most concurrency abstractions on these (and similar) platforms is that, once started, work units are almost completely unable to be forcibly cancelled (other than by terminating the process.)
The way one would generally handle this would be to check a flag from within the work units themselves that indicates that they should be canceled, at which point they can do whatever cleanup is needed (if any), and return early. If you had a dispatch group, you could associate a flag with the group, and then explicitly check the flag at the beginning of each operation (or at the top of each iteration of a loop within the operation), and returning early if the group is cancelled.
You can do this ad hoc. You can use an existing abstraction (like
NSOperation
‘scancel
method) but even there, you’ll note that the first line of the Discussion section says: "This method does not force your operation code to stop." Your operation’s code must still check the flag.For example, you could use
NSBlockOperation
, create the operation instance, then capture a zeroing, weak reference to theNSBlockOperation
instance inside the closure of the block that does the real work, and then use that reference to check whether theNSBlockOperation
has been canceled (or deallocated).Long story short, there’s no proverbial free lunch here.
Essentially, you appear to be looking for cancelable asynchronous tasks. This will be very difficult with your implementation (dispatch groups, using
wait
to block a thread until the request is done). I guess you could theoretically be done by saving theURLSessionTask
object(s) for theURLSession
requests (that I presume are buried in your “network manager”) andcancel
those when the view in question disappears. But that is inelegant.Back in the day, the advice would be to wrap your asynchronous tasks in an
Operation
subclass, assiduously avoid using dispatch groups, do the appropriateisFinished
,isExecuting
KVO notifications. YourOperation
subclass would overridecancel
to cancel the network request. See https://stackoverflow.com/a/40560463/1271826 for an example. Anyway, if you implement this sort ofOperation
subclass to wrap your asynchronous work, then canceling the work is fairly simple.But having suggested how you might do it with legacy API (namely, operation queues), I must confess that I would not consider that for a second, nowadays. Instead, we would reach for Swift concurrency. That not only manages dependencies for asynchronous tasks, but the standard
URLSession
methodsdata(from:)
anddata(for:)
natively support cancelation. (This is assuming you are even usingURLSession
; you have not shared the relevant code with us.) You will end up with code that is simpler, handles asynchronous dependencies, and supports cancelation. But it will require a more thorough rewrite of code that you have not shared, so we cannot even start to advise you on specifics. But see WWDC videos Meet async/await in Swift and Swift concurrency: Update a sample app (and other videos linked in the respective “related videos” sections therein).In the comments below, you noted that you are using Alamofire. It uses
URLSession
and supports Swift concurrency.E.g., consider this sort of legacy approach with Alamofire:
You could, instead, adopt Swift concurrency with Alamofire, and you now have a cancelable API:
With this cancelable rendition, you can then do things like:
See https://github.com/robertmryan/AlamofireWithSwiftConcurrency for Minimal, Reproducible Example contrasting GCD and Swift concurrency approaches.
Now, I recognize that this is dramatically different from your example (because you did not share enough of your implementation), so don’t get too lost in the weeds of my example, but note that: