Question or problem with Swift language programming:
I would like to simulate async and await request from Javascript to Swift 4. I searched a lot on how to do it, and I thought I found the answer with DispatchQueue, but I don’t understand how it works.
I want to do a simple stuff:
if let items = result.value { var availableBornes = [MGLPointFeature]() for item in items { guard let id = item.id else { continue } let coordinate = CLLocationCoordinate2D(latitude: Double(coor.x), longitude: Double(coor.y)) // ... // This is an asynchronous request I want to wait await _ = directions.calculate(options) { (waypoints, routes, error) in guard error == nil else { print("Error calculating directions: \(error!)") return } // ... if let route = routes?.first { let distanceFormatter = LengthFormatter() let formattedDistance = distanceFormatter.string(fromMeters: route.distance) item.distance = formattedDistance // Save feature let feature = MGLPointFeature() feature.attributes = [ "id": id, "distance": formattedDistance ] availableBornes.append(feature) } } } // This should be called after waiting for the async requests self.addItemsToMap(availableBornes: availableBornes) }
What should I do?
How to solve the problem:
Solution 1:
Thanks to vadian‘s comment, I found what I expected, and it’s pretty easy. I use DispatchGroup()
, group.enter()
, group.leave()
and group.notify(queue: .main){}
.
func myFunction() { let array = [Object]() let group = DispatchGroup() // initialize array.forEach { obj in // Here is an example of an asynchronous request which use a callback group.enter() // wait LogoRequest.init().downloadImage(url: obj.url) { (data) in if (data) { group.leave() // continue the loop } } } group.notify(queue: .main) { // do something here when loop finished } }
Solution 2:
(Note: Swift 5 may support await
as you’d expect it in ES6!)
What you want to look into is Swift’s concept of “closures”. These were previously known as “blocks” in Objective-C, or completion handlers.
Where the similarity in JavaScript and Swift come into play, is that both allow you to pass a “callback” function to another function, and have it execute when the long-running operation is complete. For example, this in Swift:
func longRunningOp(searchString: String, completion: (result: String) -> Void) { // call the completion handler/callback function completion(searchOp.result) } longRunningOp(searchString) {(result: String) in // do something with result }
would look like this in JavaScript:
var longRunningOp = function (searchString, callback) { // call the callback callback(err, result) } longRunningOp(searchString, function(err, result) { // Do something with the result })
There’s also a few libraries out there, notably a new one by Google that translates closures into promises: https://github.com/google/promises. These might give you a little closer parity with await
and async
.
Solution 3:
In iOS 13 and up, you can now do this using Combine. Future
is analogous to async
and the flatMap
operator on publishers (Future
is a publisher) is like await
. Here’s an example, loosely based on your code:
Future { promise in directions.calculate(options) { (waypoints, routes, error) in if let error = error { promise(.failure(error)) } promise(.success(routes)) } } .flatMap { routes in // extract feature from routes here... feature } .receiveOn(DispatchQueue.main) // UI updates should run on the main queue .sink(receiveCompletion: { completion in // completion is either a .failure or it's a .success holding // the extracted feature; if the process above was successful, // you can now add feature to the map }, receiveValue: { _ in }) .store(in: &self.cancellables)
Edit: I went into more detail in this blog post.
Solution 4:
You can use semaphores to simulate async/await.
func makeAPICall() -> Result { let path = "https://jsonplaceholder.typicode.com/todos/1" guard let url = URL(string: path) else { return .failure(.url) } var result: Result ! let semaphore = DispatchSemaphore(value: 0) URLSession.shared.dataTask(with: url) { (data, _, _) in if let data = data { result = .success(String(data: data, encoding: .utf8)) } else { result = .failure(.server) } semaphore.signal() }.resume() _ = semaphore.wait(wallTimeout: .distantFuture) return result }
And here is example how it works with consecutive API calls:
func load() { DispatchQueue.global(qos: .utility).async { let result = self.makeAPICall() .flatMap { self.anotherAPICall($0) } .flatMap { self.andAnotherAPICall($0) } DispatchQueue.main.async { switch result { case let .success(data): print(data) case let .failure(error): print(error) } } } }
Here is the article describing it in details.
And you can also use promises with PromiseKit and similar libraries
Solution 5:
You can use this framework for Swift coroutines – https://github.com/belozierov/SwiftCoroutine
Unlike DispatchSemaphore, when you call await it doesn’t block the thread but only suspends coroutine, so you can use it in the main thread as well.
func awaitAPICall(_ url: URL) throws -> String? { let future = URLSession.shared.dataTaskFuture(for: url) let data = try future.await().data return String(data: data, encoding: .utf8) } func load(url: URL) { DispatchQueue.main.startCoroutine { let result1 = try self.awaitAPICall(url) let result2 = try self.awaitAPICall2(result1) let result3 = try self.awaitAPICall3(result2) print(result3) } }