How do I wait for an asynchronous closure? I am making an async network request and getting its results in a closure but somewhere I have to know that my request has been completed so I can get the values
You can approach this several ways. The easiest is to post a notification or call a delegate method from your completion handler. Make sure you stay aware of the thread you’re working with. You may need to dispatch to the main thread to perform your delegation, callbacks, or notifications.
When you’re working with several tasks that must finish before moving forward, use a dispatch group to keep count until they’re all done, and then send your update information.
If you truly need to stop execution until the request returns (and this is usually a bad thing with the possible exception of synchronous execution in playgrounds), you can use a run loop to stop things until your asynchronous call returns control. This breaks a lot of best practice rules. It can be mighty handy when you don’t want to start building lots of helper classes in a quick demo or proof of concept, where the emphasis is on procedural tasks.
6 Comments
Why not use a semaphore?
some_async_call {
// some work
// semaphore signal
}
// semaphore wait
I second the semaphore solution. Also, there is nothing bad in pausing the execution if you’re already on a separate thread.
Semaphore I thought as I read the text. I see two other people got there first and I also don’t understand how the posted solution could work.
If the completion handler is stopping the right run loop, that must imply it is running on the thread that started the task (which goes on to start a run loop). For that to work in general, there must already be a run loop running on the thread,
I started with semaphore, found it wasn’t working right with the threading, and ended up with the run loops. I’m feeling too lazy right now to get it working with semaphore. If you want to go ahead and fix it, I’ll update with attribution. Have at it.
I too ran into the issue with DispatchSemaphore as you describe, but I don’t think it’s too blame. Were you doing a command line application or playground? Looking at the docs for geocodeAddressDictionary, it states that “Your completion handler block will be executed on the main thread”. Without the call to CFRunLoopRun (which I believe is implicit using an application, correct?), there is no event loop for the completion handler to be inserted into. Playing around using a semaphore in SyncMaker, putting the for loop into a DispatchQueue.async, and calling CFRunLoopRun does work. Of course that still necessitates the need to manage the run loop in this example, but in a generic application where something else is managing the run loop, the code remains portable.
import Foundation
/// A generic Cocoa-Style completion handler
typealias CompletionHandler = (T?, Error?) -> Void
/// A generic result type that returns a value or an error
enum Result { case success(T), error(Error), uninitialized }
class SyncMaker {
var semaphore = DispatchSemaphore(value: 0)
var result: Result = .uninitialized
func completion() -> CompletionHandler {
return {
(value: Value?, error: Error?) in
var result: Result = .uninitialized
// Fetch result
switch(value, error) {
case (let value?, _): result = .success(value)
case (_, let error?): result = .error(error)
default: break
}
// Store result, return control
self.result = result
self.semaphore.signal()
}
}
// Perform task (that must use custom completion handler) and wait
func run(_ task: @escaping () -> Void) -> Result {
task()
semaphore.wait()
return result
}
}
// Example of using SyncMaker
import CoreLocation
import Contacts
struct BadKarma: Error {}
func getCityState(from zip: String) throws -> String {
let geoCoder = CLGeocoder()
let infoDict = [
CNPostalAddressPostalCodeKey: zip,
CNPostalAddressISOCountryCodeKey: "US"]
let syncMaker = SyncMaker()
let result = syncMaker.run {
geoCoder.geocodeAddressDictionary(
infoDict,
completionHandler: syncMaker.completion())
}
switch result {
case .error(let complaint): throw(complaint)
case .success(let placemarks):
guard placemarks.count > 0 else { break }
let mark = placemarks[0]
guard let address = mark.addressDictionary,
let city = address["City"],
let state = address["State"]
else { break }
return "\(city), \(state)"
default: break
}
throw BadKarma()
}
let queue = DispatchQueue(label: "SynkMaker")
let loop = CFRunLoopGetCurrent()
queue.async {
do {
for zip in ["10001", "20500", "90210"] {
print("zip: \(zip), citystate:", try getCityState(from: zip))
}
} catch { print(error) }
CFRunLoopStop(loop)
}
CFRunLoopRun()
Thanks for looking into the docs. And yes, as I stated, this is primarily for playgrounds.