DispatchQueue does not wait for async function to complete - ios

I'm attempting to create a simple package tracking app that uses the USPS API to fetch data. The callRestService method successfully fetches the data and its completion handler serviceCallback (which sets the unparsedXml property) works. However, the method that I call callRestService from does not wait for it and its completion handler to complete before moving on, resulting in my print(unparsedXml) statement returning nil.
As shown below, I tried to use a DispatchGroup object and DispatchQueue to make the function wait for callRestService's completion but it continues regardless. How can I make the function wait for the call to complete?
var unparsedXml:String?
public func getTrackingInfo(_ trackingNumber: String) -> TrackingInfo {
let group = DispatchGroup()
group.enter()
DispatchQueue.global(qos: DispatchQoS.default.qosClass).async {
self.callRestService(requestUrl: self.getRequest(trackingNumber))
group.leave()
}
group.wait()
print(unparsedXml)
return TrackingInfo()
}
private func getRequest(_ trackingNumber: String) -> String {
let APIUsername = "Intentionally Omitted"
let trackingXmlLink = "http://production.shippingapis.com/ShippingAPI.dll?API=TrackV2&XML=%3CTrackFieldRequest%20USERID=%22" + APIUsername + "%22%3E%20%3CRevision%3E1%3C/Revision%3E%20%3CClientIp%3E127.0.0.1%3C/ClientIp%3E%20%3C/SourceId%3E%20%3CTrackID%20ID=%22" + trackingNumber + "%22%3E%20%3CDestinationZipCode%3E66666%3C/DestinationZipCode%3E%20%3CMailingDate%3E2010-01-01%3C/MailingDate%3E%20%3C/TrackID%3E%20%3C/TrackFieldRequest%3E"
return trackingXmlLink
}
public func callRestService(requestUrl:String) ->Void
{
var request = URLRequest(url: URL(string: requestUrl)!)
request.httpMethod = "GET"
let session = URLSession.shared
let task = session.dataTask(with: request, completionHandler: serviceCallback)
task.resume()
}
private func serviceCallback(data:Data? , response:URLResponse? , error:Error? ) -> Void
{
unparsedXml = String(data: data!, encoding: .utf8)
//print(unparsedXml) Works correctly when uncommented
}

Your problem is that callRestService will dispatch an asynchronous network operation, so your group.leave will be called immediately, firing your group.notify.
You could put the group.leave in a completion handler, but you should avoid blocking code. I would suggest you structure your getTrackingInfo as asynchronous function that takes a completion handler:
public func getTrackingInfo(_ trackingNumber: String, completion:(TrackingInfo?,Error?) -> Void) {
self.callRestService(requestUrl: self.getRequest(trackingNumber)) { (data, response, error) in
guard error == nil, let returnData = data else {
completion(nil,error)
return
}
completion(TrackingInfo(returnData),nil)
}
}
private func getRequest(_ trackingNumber: String) -> String {
let APIUsername = "Intentionally Omitted"
let trackingXmlLink = "http://production.shippingapis.com/ShippingAPI.dll?API=TrackV2&XML=%3CTrackFieldRequest%20USERID=%22" + APIUsername + "%22%3E%20%3CRevision%3E1%3C/Revision%3E%20%3CClientIp%3E127.0.0.1%3C/ClientIp%3E%20%3CSourceId%3EFaiz%20Surani%3C/SourceId%3E%20%3CTrackID%20ID=%22" + trackingNumber + "%22%3E%20%3CDestinationZipCode%3E66666%3C/DestinationZipCode%3E%20%3CMailingDate%3E2010-01-01%3C/MailingDate%3E%20%3C/TrackID%3E%20%3C/TrackFieldRequest%3E"
return trackingXmlLink
}
public func callRestService(requestUrl:String, completion:(Data? , URLResponse? , Error? ) -> Void) ->Void
{
var request = URLRequest(url: URL(string: requestUrl)!)
request.httpMethod = "GET"
let session = URLSession.shared
let task = session.dataTask(with: request, completionHandler: completion)
task.resume()
}

Wait for the completion of the operation in the notify
Simulate a network request:
public func networkTask(label: String, cost: UInt32, complete: #escaping ()->()) {
NSLog("Start network Task task%#",label)
DispatchQueue.global().async {
sleep(cost)
NSLog("End networkTask task%#",label)
DispatchQueue.main.async {
complete()
}
}
}
let group = DispatchGroup()
group.enter()
networkTask(label: "2", cost: 4) {
group.leave()
}
group.enter()
networkTask(label: "1", cost: 3) {
group.leave()
}
group.notify(queue: .main) {
print("task complete!")
// ......
}
you can try this example.

Related

URLSession does not call API. Though in Playground it works

So this code works for me in playground but for some reason URLSession.shared.dataTask(... doesnt call my flask api that im currently locally running. Any idea on what's wrong? So far I'm only concerned on why it does not enter the do{in my project but it works properly in playground.
func getWords() -> [Word]{
var words = [Word]()
let url = URL(string: self.url)
let request = URLRequest(url: url!)
let group = DispatchGroup()
print("XD")
URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
do {
print("A")
if let data = data{
print("B")
if let decodedResponse = try? JSONDecoder().decode([Word].self, from: data){
group.enter()
DispatchQueue.main.async(){
words = decodedResponse
print("C")
print(words)
group.leave()
}
}
}
print("DD")
} catch {
print("Words.swift Error in try catch")
}
group.enter()
}).resume()
group.leave()
group.notify(queue: DispatchQueue.main, execute: {
print(words)
})
print("ASDASD WORDS: \(words)")
for _ in 1 ... 4 {
// - to make sure there aren't duplicates -
var wordId:Int = Int.random(in: 0..<words.count)
while randomIds.contains(wordId){
wordId = Int.random(in: 0..<words.count)
}
randomIds.append(wordId)
}
//returns 4 words
return words
}
You aren't using DispatchGroup correctly; You should call enter before you start the asynchronous work and leave once it is complete. You can then use notify to perform some operation.
However, you don't really need a DispatchGroup in this situation; You have that because you are trying to turn an asynchronous operation into a synchronous one;
The correct approach is to accept that the operation is asynchronous and it isn't possible for this function to return [Word]. You will need to refactor the function to accept a completion handler closure and invoke that with the result.
Something like this:
func getWords(completionHandler:#escaping (Result<[Word], Error>) -> Void) -> Void{
var words = [Word]()
let url = URL(string: self.url)
let request = URLRequest(url: url!) // Note you should use a guard and call the completion handler with an error if url is `nil`
URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
if let error = error {
completionHandler(.failure(error))
} else {
do {
if let data = data {
let words = try JSONDecoder().decode([Word].self, from: data)
completionHandler(.success(words))
} else {
// TODO call completionHander with a .failure(SomeError)
}
} catch {
completionHandler(.failure(error))
}
}
}).resume()
}
Then you can call it:
getWords() { result in
switch result {
case .success(let words):
print(words)
case .failure(let error):
print(error.localizedDescription)
}
}

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

DispatchGroup not working with URLSession.shared.dataTask

I'm trying to implement many concurrent api calls through URLSession.shared singleton. What I want is that they are made concurrently so when I had all the responses, then execute the completion handler (I took this as example: https://medium.com/#oleary.audio/simultaneous-asynchronous-calls-in-swift-9c1f5fd3ea32):
init(completion: (()->Void)? = nil) throws {
self.myToken = myToken
let group = DispatchGroup()
group.enter()
print("Downloading User")
self.downloadUser(completion: { userObject in
self.userObject = userObject
group.leave()
})
group.enter()
print("Downloading Tickets")
self.downloadTickets(completion: { ticketsObject in
if let ticketsObject = ticketsObject {
self.ticketsObject = ticketsObject
}
group.leave()
})
group.wait()
completion?()
}
Then the functions the implement the api calls are something like:
private func downloadUser( completion: #escaping ((myUsuario)->Void )) {
let url = URL(string: "\(Globals.laravelAPI)me")
var request = URLRequest(url: url!)
let task = URLSession.shared.dataTask(with: request) {
(data:Data?, response:URLResponse?, error:Error?) in
if let error = error {
...
}
guard let httpResponse = response as? HTTPURLResponse,(200...299).contains(httpResponse.statusCode) else { ... }
if let data = data {
do {
let user = try JSONDecoder().decode(myUsuario.self, from: data)
completion(user)
} catch {
print("USER PARSING ERROR: \(error)")
fatalError()
}
}
}
task.resume()
}
When I run the program, it make the calls but never get the responses so the group.wait() is never executed.

Execute GET request synchronously

I have a function that sends a GET request to an API and I need to wait for the response before I can continue with the rest of my code.
I have tried various different threads but haven't got it to work. I've been trying to solve this by myself but I am quite confused by threads and so fourth.
These are my functions:
func checkDomains() {
let endings = [".com"]
for ending in endings {
let domainName = names[randomNameIndx] + ending
let urlString = "https://domainr.p.mashape.com/v2/status?mashape-key={my-key}" + domainName
let url = URL(string: urlString)
var request = URLRequest(url: url!)
request.setValue("{my-key}", forHTTPHeaderField: "X-Mashape-Key")
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.httpMethod = "GET"
var ourBool = false
DispatchQueue.main.sync {
let task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, er) in
do {
print("hey")
if let json = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? NSDictionary {
ourBool = String(describing: json).contains("inactive")
if ourBool {
self.domainStatuses.append("available")
} else {
self.domainStatuses.append("taken")
}
}
} catch {
}
})
task.resume()
}
}
}
#objc func btnTapped(sender: UIButton) {
self.prepareForNewName()
sender.shake(direction: "horizontal", swings: 1)
self.checkDomains() // This needs to finish before calling the next functions
self.setupForNewName()
self.animateForNewName()
}
My suggestion is in adding callback param into your async function. Example below:
func checkDomains(_ onResult: #escaping (Error?) -> Void)
Then you can call onResult inside your function in places where server return success result or error.
func checkDomains(_ onResult: #escaping (Error?) -> Void) {
...
let task = URLSession.shared.dataTask ... {
do {
//Parse response
DispatchQueue.main.async { onResult(nil) }
} catch {
DispatchQueue.main.async { onResult(error) }
}
...
}
}
The last thing you need pass callback param in place where you calling checkDomains function
#objc func btnTapped(sender: UIButton) {
self.prepareForNewName()
sender.shake(direction: "horizontal", swings: 1)
self.checkDomains { [unowned self] error in
if let error = error {
// handle error
return
}
self.setupForNewName()
self.animateForNewName()
}
}
Thank you for your answers. I just came home and realized there is a simpler way of doing this.
let task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
do {
...
if response != nil {
DispatchQueue.main.async {
self.setupForNewName()
self.animateForNewName()
}
}
}
} catch {
print(error)
}
})
task.resume()
Of course this is dependent on the response of the API but I am confident enough the API will always respond.
Lately it is a good practice to do network call asynchronously. There some technics to make code simpler, like https://cocoapods.org/pods/ResultPromises
TBC...

Function completes before NSURLSession - dataTaskWithRequest completed [duplicate]

This question already has an answer here:
NSURLConnection sendAsynchronousRequest can't get variable out of closure
(1 answer)
Closed 7 years ago.
I have the following method called under ViewDidLoad(). I understand that session.dataTaskWithRequest automatically runs in background thread. And because of the same, the code following this method in ViewDidLoad() does not wait for this process to complete and starts executing.
Is there any way that I can ensure that the background thread is completed before other methods are executed?
func getCoordinatesFromServer() {
let request = NSMutableURLRequest(URL: NSURL(string: constants.urlName.loadData)!)
request.HTTPMethod = "POST"
request.addValue("multipart/form-data", forHTTPHeaderField: "Accept")
request.setValue("keep-Alive", forHTTPHeaderField: "Connection")
request.HTTPBody = (constants.requestTag.getCoordinates).data
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request, completionHandler: {(data, response ,error ) in
if let response = response {
let httpResponse = response as! NSHTTPURLResponse
print("response code = \(httpResponse.statusCode)")
if (httpResponse.statusCode == 200) {
dispatch_async(dispatch_get_main_queue(), {
let decodedData = NSData(base64EncodedData: data!, options: NSDataBase64DecodingOptions([]))
let jsonText = NSString(data: decodedData!, encoding: NSASCIIStringEncoding) as! String
do {
let json = try NSJSONSerialization.JSONObjectWithData(jsonText.data, options: NSJSONReadingOptions.init(rawValue: 0))
self.parseJsonData(json["Datalist"] as! NSArray)
} catch {
print("Error:\n \(error)")
}
})
}
}
})
task.resume()
}
Regards,
if I understand your question, you can solve this problem at this way
For example:
class func ConnectedToNetwork(completionHandler: ((Status: Bool) -> Void))
{
let url = NSURL(string: "http://google.com")
let request = NSMutableURLRequest(URL: url!)
request.HTTPMethod = "HEAD"
request.timeoutInterval = 0.2
let task = NSURLSession.sharedSession().dataTaskWithURL(url!) {(data,response, error) in
let httpResponse = response as! NSHTTPURLResponse
if httpResponse.statusCode == 200
{ completionHandler(Status: true)
return
}
}
task.resume()
}
and then, you can work with this
Checks.ConnectedToNetwork({ Status in dispatch_async(dispatch_get_main_queue())
{
if Status == true
{
//do what you want
}
});
dataTaskWithRequest is assync call, and it has completionHandler block. So all code that you wrote inside will be executed after the data task finished:
let request = NSURLRequest(URL: NSURL(string: "http://google.com")!)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request, completionHandler: {(data, response ,error ) in
print("3") // executed after data task is finished
print("4") // executed after data task is finished
})
task.resume()
print("1")
print("2")
import Foundation
// some aync function with completition handler, i MUST USE AS IT IS
func f(completitionHandler: ()->()) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) { () -> Void in
print("running concurrently")
sleep(1)
print("finished")
completitionHandler()
}
}
// some function running on main queue
func foo() {
// create dispatch group
let group = dispatch_group_create()
let myCompletitionHandler: ()->() = {
dispatch_group_leave(group)
}
let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)
dispatch_group_async(group, queue) { () -> Void in
dispatch_group_enter(group)
f(myCompletitionHandler)
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER)
}
foo()
print("foo finished")
It is not the best at all, the better solution is to run synchronous version of f() instead of this 'workaround':-)

Resources