30

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?

Eric Aya
  • 69,473
  • 35
  • 181
  • 253
cusmar
  • 1,903
  • 1
  • 20
  • 39
  • There is no Javascript in your question. What exactly is the issue with the Swift code you posted? Your question is very unclear. – rmaddy Feb 09 '18 at 20:16
  • 1
    Don't wait. Please learn to understand asynchronous data processing in Swift. In your example just put the line `self.addItemsToMap(availableBornes: availableBornes)` **into** the completion block. – vadian Feb 09 '18 at 20:18
  • No I can’t because I have to wait the for to be finished. My asynchronous query will be called items.count times. – cusmar Feb 09 '18 at 20:23
  • 2
    Then use `DispatchGroup` . It provides `enter` and `leave` statements and can `notify` when the last iteration has finished. Please have a look at https://stackoverflow.com/questions/44912000/how-to-set-up-dispatchgroup-in-asynchronous-iteration – vadian Feb 09 '18 at 20:26
  • It sounds like you want a DispatchGroup. you'll never use "promises" again once you try out normal computer code :-O ;) – Fattie May 25 '20 at 16:57
  • ah as Vadian already said .. DispatchGroup – Fattie May 25 '20 at 16:57

9 Answers9

46

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
    }
}
cusmar
  • 1,903
  • 1
  • 20
  • 39
  • 1
    This is also a non-answer, because the function will return before the code in group.notify runs – scaly Jun 11 '21 at 21:45
9

We have to await!

The async-await Swift Evolution proposal SE-0296 async/await was accepted after 2 pitches and revision modifications recently on December 24th 2020. This means that we will be able to use the feature in Swift 5.5. The reason for the delay is due to backwards-compatibility issues with Objective-C, see SE-0297 Concurrency Interoperability with Objective-C. There are many side-effects and dependencies of introducing such a major language feature, so we can only use the experimental toolchain for now. Because SE-0296 had 2 revisions, SE-0297 actually got accepted before SE-0296.

General Use

We can define an asynchronous function with the following syntax:

private func raiseHand() async -> Bool {
  sleep(3)
  return true
}

The idea here is to include the async keyword alongside the return type since the call site will return (BOOL here) when complete if we use the new await keyword.

To wait for the function to complete, we can use await:

let result = await raiseHand()

Synchronous/Asynchronous

Defining synchronous functions as asynchronous is ONLY forward-compatible - we cannot declare asynchronous functions as synchronous. These rules apply for function variable semantics, and also for closures when passed as parameters or as properties themselves.

var syncNonThrowing: () -> Void
var asyncNonThrowing: () async -> Void
...
asyncNonThrowing = syncNonThrowing // This is OK.

Throwing functions

The same consistency constraints are applied to throwing functions with throws in their method signature, and we can use @autoclosures as long as the function itself is async.

We can also use try variants such as try? or try! whenever we await a throwing async function, as standard Swift syntax.

rethrows unfortunately still needs to go through Proposal Review before it can be incorporated because of radical ABI differences between the async method implementation and the thinner rethrows ABI (Apple wants to delay the integration until the inefficiencies get ironed out with a separate proposal).

Networking callbacks

This is the classic use-case for async/await and is also where you would need to modify your code:

// This is an asynchronous request I want to wait
await _ = directions.calculate(options) { (waypoints, routes, error) in

Change to this:

func calculate(options: [String: Any]) async throws -> ([Waypoint], Route) {
    let (data, response) = try await session.data(from: newURL)
    // Parse waypoints, and route from data and response.
    // If we get an error, we throw.
    return (waypoints, route)
}
....
let (waypoints, routes) = try await directions.calculate(options)
// You can now essentially move the completion handler logic out of the closure and into the same scope as `.calculate(:)`

The asynchronous networking methods such as NSURLSession.dataTask now has asynchronous alternatives for async/await. However, rather than passing an error in the completion block, the async function will throw an error. Thus, we have to use try await to enable throwing behaviour. These changes are made possible because of SE-0297 since NSURLSession belongs to Foundation which is still largely Objective-C.

Code impacts

  • This feature really cleans up a codebase, goodbye Pyramid of Doom !

  • As well as cleaning up the codebase, we improve error handling for nested networking callbacks since the error and result are separated.

  • We can use multiple await statements in succession to reduce the dependency on DispatchGroup. to Threading Deadlocks when synchronising DispatchGroups across different DispatchQueues.

  • Less error-prone because the API is clearer to read. Not considering all exit paths from a completions handler, and conditional branching means subtle bugs can build up that are not caught at compile time.

  • async / await is not back-deployable to devices running < iOS 13, so we have to add if #available(iOS 13, *) checks where supporting old devices. We still need to use GCD for older OS versions.

Pranav Kasetti
  • 8,770
  • 2
  • 50
  • 71
6

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

brandonscript
  • 68,675
  • 32
  • 163
  • 220
  • Thanks! I thought about using callbacks, but my code is a little bit complex because I have a for loop which contains asynchronous requests. I want to execute the code outside the for loop after asynchronous requests has been done. I don't see how I could use callbacks here because I should call it many times. Do you understand what I mean? – cusmar Feb 09 '18 at 20:55
  • Yep, then what you’re looking for is a promise. Check that link there. – brandonscript Feb 09 '18 at 21:14
6

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)
    }
}
Alex Belozierov
  • 131
  • 1
  • 4
6

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<Feature, Error> { 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.

Bill
  • 44,502
  • 24
  • 122
  • 213
  • This doesn't allow you to "await" anything, the function that this happens in will still happen asynchronously, meaning you can't return the result from the function this code is in. That makes this a non-answer to the question – scaly Jun 11 '21 at 21:43
5

You can use semaphores to simulate async/await.

func makeAPICall() -> Result <String?, NetworkError> {
            let path = "https://jsonplaceholder.typicode.com/todos/1"
            guard let url = URL(string: path) else {
                return .failure(.url)
            }
            var result: Result <String?, NetworkError>!
            
            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

SergPanov
  • 269
  • 4
  • 7
1

Async/await is now officially supported in Swift.

It would yield be something like this

func myFunction() async throws {
    let array: [Object] = getObjects()

    let images = try await withThrowingTaskGroup(of: Data.self, returning: [Data].self) { group in
        array.forEach { object in
            group.async {
                try await LogoRequest().downloadImage(url: object.url)
            }
        }
        return try await group.reduce([], {$0 + [$1]})
    }
    // at this point all `downloadImage` are done, and `images` is populated
    _ = images
}
PierreMB
  • 94
  • 4
0

Use async/ await below like this,

enum DownloadError: Error {

case statusNotOk
case decoderError

}

Method Call

override func viewDidLoad()  {
    super.viewDidLoad()
    async{
        do {
            let data =  try await fetchData()
            do{
                let json = try JSONSerialization.jsonObject(with: data, options:.mutableLeaves)
                print(json)
            }catch{
                print(error.localizedDescription)
            }
        }catch{
            print(error.localizedDescription)
        }
        
    }
}

func fetchData() async throws -> Data{
    
    let url = URL(string: "https://www.gov.uk/bank-holidays.json")!
    let request = URLRequest(url:url)
    let (data,response) = try await URLSession.shared.data(for: request)
    
    guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else{
        
        throw DownloadError.statusNotOk
    }
    return data
    
}
0

Here you can see a basic difference, how the callBack replacing by async/await

Here you can see a basic difference, how the callBack replacing by async/await

Basically, scene A and scene B = Call API by the closure. Scene C and scene D= Calling API by Async/Await. Scene E = Serial API call by nested closure. Scene F= Serial API call by Async/Await. Scene G = Parallel API call by Async/Await.

Parallel API call

enter image description here

Niraj Paul
  • 1,498
  • 14
  • 22
  • This Medium article for more info: https://medium.com/@nirajpaul.ios/async-await-actors-in-swift-e2941e5f6dbf – Niraj Paul Apr 11 '22 at 02:56