skip to Main Content

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


  1. 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‘s cancel 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 the NSBlockOperation instance inside the closure of the block that does the real work, and then use that reference to check whether the NSBlockOperation has been canceled (or deallocated).

    Long story short, there’s no proverbial free lunch here.

    Login or Signup to reply.
  2. 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 the URLSessionTask object(s) for the URLSession requests (that I presume are buried in your “network manager”) and cancel 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 appropriate isFinished, isExecuting KVO notifications. Your Operation subclass would override cancel to cancel the network request. See https://stackoverflow.com/a/40560463/1271826 for an example. Anyway, if you implement this sort of Operation 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 methods data(from:) and data(for:) natively support cancelation. (This is assuming you are even using URLSession; 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:

    class LegacyNetworkManager {
        static let shared = LegacyNetworkManager()
    
        private init() { }
    
        func fetch(completion: @escaping (Result<[Post], AFError>) -> Void) {
            AF.request("https://jsonplaceholder.typicode.com/posts")
                .response(responseSerializer: .decodable(of: [Post].self)) { response in
                    completion(response.result)
                }
        }
    }
    

    You could, instead, adopt Swift concurrency with Alamofire, and you now have a cancelable API:

    actor NetworkManager {
        static let shared = NetworkManager()
    
        private init() { }
    
        func posts() async throws -> [Post] {
            try await AF.request("https://jsonplaceholder.typicode.com/posts")
                .serializingDecodable([Post].self)
                .value
        }
    }
    

    With this cancelable rendition, you can then do things like:

    import os.log
    
    private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "SwiftConcurrencyViewController")
    
    class SwiftConcurrencyViewController: UIViewController {
        @IBOutlet weak var tableView: UITableView!
        
        private let networkManager = NetworkManager.shared
        private let databaseManager = DatabaseManager.shared
    
        private var task: Task<Void, Error>?
    
        var posts: [Post] = []
    
        override func viewDidLoad() {
            super.viewDidLoad()
            tableView.register(UINib(nibName: "PostCell", bundle: nil), forCellReuseIdentifier: "PostCell")
        }
        
        // start request when view appears …
    
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
    
            logger.debug("launching task")
            task = Task { await fetch() }
        }
    
        // … but if not finished by the time the view disappears, cancel it
    
        override func viewWillDisappear(_ animated: Bool) {
            super.viewWillDisappear(animated)
    
            logger.debug("canceling (if not already done)")
            task?.cancel()
        }
    }
    
    extension SwiftConcurrencyViewController: UITableViewDataSource {
        …
    }
    
    private extension SwiftConcurrencyViewController {
        func fetch() async {
            do {
                logger.debug("fetching")
                posts = try await networkManager.posts()
                logger.debug("fetched")
                tableView.reloadData()
                try await databaseManager.save(posts)
            } catch {
                logger.error("(error)")
                show(error.localizedDescription)
            }
        }
    }
    

    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:

    • Even in the GCD example, we would not use dispatch groups; but
    • By adopting Swift concurrency, we can not only manage dependencies between asynchronous tasks, but also enjoying cancelation logic.
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search