简体   繁体   中英

Cancel DispatchWorkItem outside loop - Swift

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.

ps: 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.

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 (eg 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).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM