How to make async / await in Swift?

i0S Swift Issue

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

Hope this helps!