Kim
Kim

Reputation: 956

Swift equivalent of await Promise.all

I'm coming from JS and learning Swift to build an iOS native version of an app.

In JS, I use the following pattern all the time:

...
async function doAyncFunction(item) {
  try {
    // do async call to fetch data using item
    return Promise.resolve(data);
  } catch (error) {
    return Promise.reject(error);
  }
}

const promises = items.map((item) => doAyncFunction(item));
const results = await Promise.all(promises);
...

I've started looking at PromiseKit, but I'm wondering what's are the Swift ways of doing this?

Thanks.

Upvotes: 1

Views: 5866

Answers (5)

iUrii
iUrii

Reputation: 13828

You can look at PromiseQ it's javascript style promises for Swift. It implements all javascript's Promise features: resolve/reject, then, finally, fetch etc. and appends some additional ones: suspend/resume, cancel, retry, timeout etc.

It also supports all, race, any e.g.:

// Fetch avatars of first 30 GitHub users.

struct User : Codable {
    let login: String
    let avatar_url: String
}

async {
    let response = try fetch("https://api.github.com/users").await()
    guard response.ok else {
        throw response.statusCodeDescription
    }

    guard let data = response.data else {
        throw "No data"
    }

    let users = try JSONDecoder().decode([User].self, from: data)

    let images =
        try Promise.all(
            users
            .map { $0.avatar_url }
            .map { fetch($0) }
        ).await()
        .compactMap { $0.data }
        .compactMap { UIImage(data: $0) }

    async(.main) {
        print(images.count)
    }
}
.catch { error in
    print(error.localizedDescription)
}

Swift's concurrency such as Dispatch queues, Combine and the newest async\await (Swift 5.5) are different from javascript Promises and you can not find many convenient approaches that you used before.

Upvotes: 1

Rob
Rob

Reputation: 437947

The forthcoming Swift 5.5 in Xcode 13 (still in beta at this point in time) uses a very similar async-await pattern. See The Swift Programming Language: Concurrency.

In the interim, there are a unfortunately dizzying number of alternatives. For example, there are a variety of third-party promise/future frameworks. Or there is the declarative Combine framework, which was launched a few years agar with the advent of the non-imperative patterns of SwiftUI.

All of that having been said, the most common pattern you’ll see in Swift code is the use of escaping “closures” which are effectively units of code that are passed as a parameter to a function, and which the function invokes when the asynchronous task completes. In that pattern you don’t await, but rather just specify what you want to do when the asynchronous task finishes. For example, in this function, it has a parameter called completion which is a closure that is called when the asynchronous task completes:

func fetch(using value: Int, completion: @escaping (Result<Foo, Error>) -> Void) {
   let url = …
   let task = URLSession.shared.dataTask(with: url) { data, response, error in
       // handle errors, if any, e.g.:

       if let error == error else {
           completion(.failure(error))
           return
       }
 
       // parse data into `Foo` here, and when done, call the `completion closure:

       …

       completion(.success(foo))
    }
    task.resume()
}

And then you would call it like so:

fetch(using: 42, completion: { result in
    // this is called when the fetch finishes
   
    switch result {
    case .failure(let error): // do something with `error`
    case .success(let foo):   // do something with `foo`
    }
})

// note, execution will continue here, and the above closure will
// be called later, so do not try to use `foo` here

Or, using a more concise “trailing closure” syntax:

fetch(using: 42) { result in
    // this is called when the fetch finishes
   
    switch result {
    case .failure(let error): // do something with `error`
    case .success(let foo):   // do something with `foo`
    }
}

// note, execution will continue here, and the above closure will
// be called later, so do not try to use `foo` here

And if you wanted to be notified when a series of calls was done, you could use a DispatchGroup, e.g.

let group = DispatchGroup()
for value in values {
    group.enter()
    fetch(using: value) { result in
        // do something with result
        group.leave()
    }
}

group.notify(queue: .main) {
    // this is called when every `enter` call is matched up with a `leave` Call
}

It is up to you whether you stick to the beta version of Swift 5.5 with a very familiar async-await pattern, use a third-party future/promise library, use Combine, or use the traditional closure-based pattern, shown above.

At the very least, I would suggest familiarizing yourself with this latter pattern as it is the predominant technique in Swift right now. But rest assured that the familiar async-await pattern is coming soon, so if you are willing to wait for it to finish going through the beta process (or join that beta process), then check that out.

Upvotes: 2

Kim
Kim

Reputation: 956

I'm answering myself here with a solution, using PromiseKit, in case it might help someone.

The below is obviously not a full implementation, but it shows how the pattern can be implemented.

func doManyAsyncRequests(userIds: [String], accessToken: String) -> Promise<Void> {
  Promise { seal in
    let promises = spotifyUserIds.map {
      doSingleAsyncRequest(userId: $0.id, accessToken: accessToken) // this function returns a promise
    }
    when(fulfilled: promises).done { results in
      print("Results: \(results)")
      // process results
    }.catch { error in
      print("\(error)")
      // handle error
    }
  }
}

Upvotes: 0

Mickael Belhassen
Mickael Belhassen

Reputation: 3342

For the moment there is a great framework that is closest to async/await, it's SwiftCoroutine https://github.com/belozierov/SwiftCoroutine (much better than promiseKit, I tested the 2..)

Swift coroutine with your example:

func doFutureFunction() -> CoFuture<Int> {
  CoFuture { promise in
      myRequest { error, data in
          if let error = error {
              promise(.failure(error))
          } else {
              promise(.success(data))
          }
      }
  }
}

let futures = items.map { item in doFutureFunction(item) } // [CoFuture<Int>]

DispatchQueue.main.startCoroutine {
    let results = promises.compactMap { try? $0.await() } // [Int]
}

The equivalent of

consts value = await future.value
consts value1 = await future.value
consts value2 = await future.value
console.log("Value " + value + ", value1 " + value1 + ", value2 " + value2)

is

DispatchQueue.main.startCoroutine {
    do {
       let value = try future.await()
       let value1 = try future.await()
       let value2 = try future.await()
       print("Value \(value), value1 \(value1), value2 \(value2)")
    } catch {
       print(error.localizedDescription)
    }
}

While waiting for swift 5.5 and official async / await from Apple

Upvotes: 1

Schottky
Schottky

Reputation: 2024

Using the aforementioned builtin Combine framework, you have several options. The one that you probably want is Publishers.Merge:

let publishers = ... // multiple functions that implement the Publisher protocol
let combined = Publishers.MergeMany(publishers) 

Alternatives to MergeMany are Merge, Merge3, Merge4 up to Merge8 when the amount of publishers is set. If the number of outputs is variable, use MergeMany.

Other options include merge on the publishers themselves:

let publisher1 = ...
let publisher2 = ...

publisher1.merge(publisher2)

CombineLatest or, in the case of a publisher that immediately completes, Zip can be used to receive a tuple when everything is done:

let publisher1 = ...
let publisher2 = ...

Publishers.CombineLatest(publisher1, publisher2)

Upvotes: 1

Related Questions