0

I've been using DispatchWorkItem and DispatchQueue to make async requests in my app. However, I've ran into trouble when trying to abort one of the requests.

I successfully use the workItem.cancel() to change the flag and I then check it where I want to abort. Like this:

for stop in self.userSettingsController.history {
    stop.PassingInfo?.removeAll()
                 
    if workItem?.isCancelled ?? false {
       print("CANCELED")
       workItem = nil
       break
    }

...

However there's one case where I have no loop in which I can keep checking if the cancelled flag changes, so I cannot abort the request using the process above. Here's the code:

  let tripQueue = DispatchQueue(label: "tripQueue")
  var tripWorkItem: DispatchWorkItem? = nil
        
  tripWorkItem = DispatchWorkItem {
            self.soapServices.GetPathsByLineAndDirection(lineCode: self.lineCode!, direction: passingInfo.Direction!) { response in
                DispatchQueue.main.async {
                    self.linePaths = response?.filter({$0.Places.contains(where: {$0.Code == self.singleStopSelected?.Code})})
                    
                    if realTime {
                        //Get estimated trip
                        self.showingRealTime = true
                        
                        if self.linePaths?.count ?? 0 > 0 {
                            self.getEstimatedTrip(lineCode: self.lineCode ?? "", direction: passingInfo.Direction ?? 0, stopCode: self.singleStopSelected?.Code ?? "", path: (self.linePaths?.first)!) { updateTripTimes in
                                //Does not work, as is expected. Just showing what I would like to achieve
                                if tripWorkItem?.isCancelled ?? false {
                                    tripWorkItem = nil
                                    return
                                }
                                
                                if updateTripTimes {
                                    DispatchQueue.main.async {
                                        self.updateTripTimes = true
                                    }
                                }
                            }
                        }
                    } else {
                        //Get trip
                        self.showingRealTime = false
                        self.getTrip(tripId: passingInfo.Id!)
                    }
                }
            }
            
            tripWorkItem = nil
        }
        
        self.currentTripWorkItem = tripWorkItem
        tripQueue.async(execute: tripWorkItem ?? DispatchWorkItem {})

Is there any way to do this?

Thanks in advance.

p.s: I'm sorry if this is duplicated, but I searched before and I couldn't find the question. I might be using the wrong terms.

kyrers
  • 489
  • 1
  • 6
  • 22
  • If you can't periodically check `isCancelled`, you can't use this `DispatchWorkItem` pattern. The question is, what are you doing in this case where you don't have the loop? If you're doing things like network requests or other sorts of things that are cancelable, there are solutions for that problem. But we can't answer this question in the abstract as there is no single solution. You have to tell us what this other use-case is doing, and then we can advise. – Rob Sep 19 '20 at 00:13
  • @Rob sorry for only getting back to you now. I'm actually doing network requests and changing state variables. I updated my question with some more code, please tell me if this is what you're looking for. – kyrers Sep 21 '20 at 10:58
  • Yep, that helps. See my answer below. But I worry about your `updateTripTimes` Boolean. I don't see you actually updating the trip times. Are you updating model objects inside `getEstimatedTrip`? If so, I'd advise against that. You can run into thread-safety and/or timing related issues. You should always have asynchronous methods pass the new data back in completion handlers and never update model objects directly. – Rob Sep 21 '20 at 16:32
  • Yes, I'm updating the objects inside ``getEstimatedTrip``. To be honest, this is my first time working with iOS development and I was, and still am, following a tight schedule. I still have improvements to make later, and that is one of that. Thanks for the advice! – kyrers Sep 22 '20 at 10:34

1 Answers1

1

Rather than putting your code in a DispatchWorkItem, consider wrapping it in a Operation subclass. You get the same isCancelled Boolean pattern:

class ComputeOperation: Operation {
    override func main() {
        while ... {
            if isCancelled { break }

            // do iteration of the calculation
        }

        // all done
    }
}

For your network request, wrap it in an custom AsynchronousOperation subclass (e.g. this implementation), and implement cancel which will cancel the network request. For example:

enum NetworkOperationError: Error {
    case unknownError(Data?, URLResponse?)
}

class NetworkOperation: AsynchronousOperation {
    var task: URLSessionTask!

    init(url: URL, completion: @escaping (Result<Data, Error>) -> Void) {
        super.init()
        self.task = URLSession.shared.dataTask(with: url) { data, response, error in
            guard
                let responseData = data,
                let httpResponse = response as? HTTPURLResponse,
                200 ..< 300 ~= httpResponse.statusCode,
                error == nil
            else {
                DispatchQueue.main.async {
                    completion(.failure(error ?? NetworkOperationError.unknownError(data, response)))
                    self.finish()
                }
                return
            }

            DispatchQueue.main.async {
                completion(.success(responseData))
                self.finish()
            }
        }
    }

    override func main() {
        task.resume()
    }

    override func cancel() {
        super.cancel()
        task.cancel()
    }
}

Don't get lost in the details of the above example. Just note that

  • It subclassed AsynchronousOperation;
  • In the completion handler, it calls finish after calling the completion handler; and
  • The cancel implementation cancels the asynchronous task.

Then you can define your queue:

let queue = OperationQueue()
queue.maxConcurrentOperationCount = 4       // use whatever you think is reasonable

And add your operations to it:

let operation = NetworkOperation(url: url) { response in
    switch response {
    case .failure(let error):
        // do something with `error`

    case .success(let data):
        // do something with `data`
    }
}
queue.addOperation(operation)

Now, the issue in your GetPathsByLineAndDirection and getEstimatedTrip is that you're not following “cancelable” patterns, namely you don't appear to be returning anything that could be used to cancel the request.

So, let's look at an example. Imagine you had some trivial method like:

func startNetworkRequest(with url: URL, completion: @escaping (Data?, URLResponse?, Error?) -> Void) {
    let task = URLSession.shared.dataTask(with: url) { data, response, error in
        completion(data, response, error)
    }
    task.resume()
}

What you'd do is change it to return something that can be canceled, the URLSessionTask in this example:

@discardableResult
func startNetworkRequest(with url: URL, completion: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionTask {
    let task = URLSession.shared.dataTask(with: url) { data, response, error in
        completion(data, response, error)
    }
    task.resume()
    return task
}

Now that’s an asynchronous task that is cancelable (and you can wrap it with the above AsynchronousOperation pattern).

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Thank you! Right now I need to develop other things in the app, but as soon as I can, I will give this a try and accept it as the correct answer. – kyrers Sep 22 '20 at 10:35