16

I have the following code:

func completeLoadAction(urlString:String) -> Int {
    let url = URL(string:urlString.trimmingCharacters(in: .whitespaces))
    let request = URLRequest(url: url!)
    let task = URLSession.shared.dataTask(with: request) { data, response, error in
        guard let data = data, error == nil else {                                                 // check for fundamental networking error
            print("error=\(error)")
            let ac = UIAlertController(title: "Unable to complete", message: "The load has been added to the completion queue. This will be processed once there is a connection.", preferredStyle: .alert)
            ac.addAction(UIAlertAction(title: "OK", style: .default))
            self.present(ac, animated:  true)
            return
        }

    let httpStatus = response as? HTTPURLResponse
        var httpStatusCode:Int = (httpStatus?.statusCode)!

        let responseString = String(data: data, encoding: .utf8)
        print("responseString = \(responseString)")
        let ac = UIAlertController(title: "Completed Successfully", message: "The "+coldel+" has been completed successfully", preferredStyle: .alert)
        ac.addAction(UIAlertAction(title:"Continue", style: .default, handler:  { action in self.performSegue(withIdentifier: "segueConfirmedLoad", sender: self) }))

        self.present(ac, animated: true)

    }
    task.resume()
    return httpStatusCode
}

I need to be able to call this and at the same time check the return value as it is the http status code, it will let me know if the call was successful or not.

Problem is because it's in a dataTask I can't access the responses status code here

var httpStatusCode:Int = (httpStatus?.statusCode)!

Because the task doesn't start until Task.Resume() is called and the task is asynchronous so it will never work.

Are there any ways around this?

Alec.
  • 5,371
  • 5
  • 34
  • 69
  • Why do you need to make it synchronous? – Larme Nov 08 '16 at 16:08
  • I need to check the response code from the http request. Was my understanding that couldn't be done if it was async – Alec. Nov 08 '16 at 16:09
  • 1
    It can be done async, you can just remove the semaphores and check but then you would need a completion block to call instead of calling the return as shown in the block below. – darren102 Nov 08 '16 at 16:13
  • Unrelated, if you're going to update the UI (e.g. present alert), this must be dispatched to the main queue (e.g. `DispatchQueue.main.async { ... }`). This completion handler runs on a background thread, but UI updates must happen on main thread. – Rob Nov 08 '16 at 16:24
  • use RxSwift / RxCocoa – aznelite89 Jan 26 '17 at 04:10
  • Possible duplicate of [NSURLSession with NSBlockOperation and queues](https://stackoverflow.com/questions/21198404/nsurlsession-with-nsblockoperation-and-queues) – eonil Oct 15 '19 at 10:50
  • If semaphore based approach doesn't work, try polling based approach: https://stackoverflow.com/a/58392835/246776 – eonil Oct 15 '19 at 10:51

5 Answers5

44

To make it synchronous and wait you can use semaphores such as below

struct Login {

    static func execute() -> Bool {
        let request = NSURLRequest....

        var success = false
        let semaphore = DispatchSemaphore(value: 0)
        let task = URLSession.shared.dataTask(with: request, completionHandler: { _, response, error in
            if let error = error {
                print("Error while trying to re-authenticate the user: \(error)")
            } else if let response = response as? HTTPURLResponse,
                300..<600 ~= response.statusCode {
                    print("Error while trying to re-authenticate the user, statusCode: \(response.statusCode)")
            } else {
                success = true
            }
            semaphore.signal()
        }) 

        task.resume()
        _ = semaphore.wait(timeout: DispatchTime.distantFuture)
        return success
    }
}
darren102
  • 2,830
  • 1
  • 22
  • 24
  • 12
    Why is it wrong? You give a it is wrong but do not back it up with anything. Synchronous networking has its place in some situations i have found and i use it. However the majority of the time asynchronous networking is used. Also the question was about synchronous networking hence the reason i provided a synchronous networking response example. Also in the comments for the question i informed that asynchronous could get the same result with a completion block. Thanks for the down votes for providing the answer the user asked per the question. – darren102 Nov 08 '16 at 19:04
  • 2
    You are correct, there are cases where one needs synchronous networking. This was not one of them. Usually, when someone asks how to "wait for network request", it's simply because they are not familiar or comfortable with asynchronous patterns, not because they really needed synchronous code. IMHO, synchronous patterns introduce so many potential problems that it's imprudent to show them how to do it without some discussion of the dangers. Apple retired the synchronous networking API for a reason. – Rob Nov 09 '16 at 19:39
  • 10
    Rob i understand that hence why i stated in the comment to the question it could be done asynchronously. I just find it weird to be told i am wrong and down voted for giving the answer to the question asked. Anyway not worrying about it, just thought it weird to be down voted for providing a valid answer to the asked question. – darren102 Nov 09 '16 at 20:10
  • @darren102 The advantage to this is that this question gets indexed by Google search, which doesn't understand the user's post (along with most readers who use read the title only). Not the right answer to the user, but useful "for the record". – user1122069 Apr 19 '17 at 19:24
  • 3
    Agree with @darren102: I'm writing a test and need to fetch data from a backdoor server which I then verify as part of the test. I found this answer through Google and the headline was exactly what I was looking for. I appreciate the original question might not have been the right question to ask, but this answers it correctly. – Hai Feb 13 '18 at 11:09
  • 1
    My scenario: I have a long async block working with all kind of images and video conversions etc going on while the app needs to work perfectly without letting the user know all this. While doing this process I have to do some reverse geocoding. so I need it to happen synchronously in my async process. (Am I making sense? ) This worked just fine for me. – Deepukjayan Mar 01 '18 at 07:58
  • this is concise and exactly what i needed for pinging an AP that the phone connects to directly (5ms response). if people remember curl in any language they should realise that async/promises are just patterns. sure all synch calls can be converted to async but often not worth the ugly code – Fakeer Apr 04 '19 at 00:37
  • This pattern doesn't work anymore with Xcode 11. Produces `nw_connection_copy_protocol_metadata [C2] Client called nw_connection_copy_protocol_metadata on unconnected nw_connection` error. – eonil Oct 15 '19 at 06:02
11

There is always a way to use the asynchronous pattern.

To make the function asynchronous add a completion block

func completeLoadAction(urlString:String, completion: (Int) -> ()) {
   let url = URL(string:urlString.trimmingCharacters(in: .whitespaces))
   let request = URLRequest(url: url!)
   let task = URLSession.shared.dataTask(with: request) { data, response, error in
      guard let data = data, error == nil else {                                                 // check for fundamental networking error
         print("error=\(error)")
         DispatchQueue.main.async {
            let ac = UIAlertController(title: "Unable to complete", message: "The load has been added to the completion queue. This will be processed once there is a connection.", preferredStyle: .alert)
            ac.addAction(UIAlertAction(title: "OK", style: .default))
            self.present(ac, animated:  true)
         }
         completion(0) // or return an error code 
         return     
      }

      let httpStatus = response as? HTTPURLResponse
      var httpStatusCode:Int = (httpStatus?.statusCode)!

      let responseString = String(data: data, encoding: .utf8)
      print("responseString = \(responseString)")
      DispatchQueue.main.async {
         let ac = UIAlertController(title: "Completed Successfully", message: "The "+coldel+" has been completed successfully", preferredStyle: .alert)
         ac.addAction(UIAlertAction(title:"Continue", style: .default, handler:  { action in self.performSegue(withIdentifier: "segueConfirmedLoad", sender: self) }))
         self.present(ac, animated: true)
      }
      completion(httpStatusCode)
   }
   task.resume()

}

and call it thusly

completeLoadAction(urlString: "www.something.com") { code in
   print(code)
}
Tony Adams
  • 691
  • 1
  • 9
  • 29
vadian
  • 274,689
  • 30
  • 353
  • 361
  • How would you be able to handle UI Updates, if at all, using this implementation. For a project I am working on I would like to update a progress bar when before and after the request, and then while parsing... – dovedevic Nov 23 '16 at 01:06
  • 1
    @DoveDevic You can update the UI in the completion block of `URLSession` for example instead of the alert messages or in the completion block of the call instead of the `print` line. A progress bar is possible if your parsing code uses a repeat loop. But normally it's too fast to use a real bar. I'd recommend to use the indeterminate circle indicator. – vadian Nov 23 '16 at 05:05
2

You can use DispatchGroup for sync network calls. More thread safe.

        let group = DispatchGroup()
        for i in 0...100 {
          group.enter()
          URLSession.shared.dataTask(with: NSURL(string: "___URL_STRING___")! as URL, completionHandler: { (data, response, error) -> Void in
                defer { group.leave() }
                print("json:\(i)")
          }).resume()
          group.wait()
        }
LoGoCSE
  • 558
  • 5
  • 13
0

This won't work in all situations. Suppose you are implementing a shared extension. And you are overriding the isContentValid() method that returns a boolean (true if the content is valid)... but in order to test if the content is valid, you want to verify that the server is running (this is a contrived example). If you make an asynchronous http call--completion block or not--you cannot return the proper value of the boolean; the only way to accomplish this is to do a synchronous call and return true/false based on the return value.

The answer that posted the semaphore pattern is the proper one to use in this case.

  • ***In this case*** the semaphore is the **wrong** solution. The question is clearly not about a situation where a synchronous request is indispensable – vadian Dec 16 '17 at 18:14
0

Complementary things to @darren102 answer using Semaphore:

If you find the solution using DispatchSemaphore as the right fit for you (for whatever reason) keep in mind that you can end up in deadlock (your signal will never be called) if the the wait and signal are called on the same thread, e.g. main queue, or some other serial queue. This can easily happen without you noticing it looking at your code. Therefore alsways add some timeout for your request so your app does not freeze, lets say 5 seconds.

_ = semaphore.wait(timeout: 5)

Also if you happen to call this custom made Semaphore syncronous function inside some closure of asychronous URL request, you can end up with signal not called (either by dispatching to the same queue or by going out of threads with thread explosion, so basically your system will wait on the same thread to be realeased, but this never happens). You will spot this problem by your semaphore waiting , and proceeding after timeout. If you want to avoid this kind of situation, call the execute() function on concurrent queue (e.g. global() ) , here is example:

func logout(_ completion: @escaping () -> ()) {      
    sendUrlRequest() { [weak self] (response, error) -> () in
        
        //do some stuff
        completion()
    }
}

func hideLoading() {
    DispatchQueue.main.async {
        //does hide loading
    }
}

func execute() -> Bool {
        let request = NSURLRequest....

        var success = false
        let semaphore = DispatchSemaphore(value: 0)
        let task = URLSession.shared.dataTask(with: request, completionHandler: { _, response, error in
            if let error = error {
                print("Error while trying to re-authenticate the user: \(error)")
            } else if let response = response as? HTTPURLResponse,
                300..<600 ~= response.statusCode {
                    print("Error while trying to re-authenticate the user, statusCode: \(response.statusCode)")
            } else {
                success = true
            }
            semaphore.signal()
        })

        task.resume()
        _ = semaphore.wait(timeout: 5)
        return success
    }

Calling execute() on concurrent queue alongside other stuff:

 logout { [weak self] in
    DispatchQueue.global().async { //calling the whole logout scope on concurrent queue
        _ = self?.execute()
            self?.hideLoading()
    }
}

Hopefully this has been at least a bit helpfull as a complementary answer. However above all my biggest recommendation is not using such semaphore synchronous code and stick to old fashioned completion in closure (as Apple recommends) and is written in this answer

Mr.SwiftOak
  • 1,469
  • 3
  • 8
  • 19