Simultaneous Asynchronous Calls in Swift

Alright loyal readers. This weeks topic is a big one. This may be a two-parter. We’ll see how far I get. There is an interesting, very real world problem that I encountered in a project that I’m working on. I’m gonna tell you all about it.

In my project, I’m using the Spotify SDK to allow users to search for and play back music. The Spotify API has a handy, server-side search function. So to search for a track in our app, I would make a function like this:

func query(text: String, completion([Track]) -> Void) {
   let endpoint = "https://www.something.iforget/search"
   let url = URL(string: endpoint)
var request = URLRequest(url: url)
   request.httpMethod = "GET"
request.addValue(//whatever header fields are needed, I forget)
let task = URLSession.shared.dataTask(with: request, completionHandler: {(data, response, error) in
      if let data = data {
         let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] ?? [:]
//Here I parse the json into an array of Tracks
completion([tracks])
}
   })
task.resume()
}

Don’t hold me to syntax here, but you should get the general idea. We have a function that sends a GET request to the API, and returns an array of Tracks through a closure. Cool.

But! I don’t just want the user to be able to search for tracks. When the user types in a search, I also want them to get back Artists and Albums. You can search for both of these in the Spotify API with the same function, just using a different endpoint. So we could totally do something like this:

func query(artist: String, completion:([Artist]) -> Void){
   //...
}
func query(album: String, completion:([Album]) -> Void){
   //...
}
func query(track: String, completion: ([Track]) -> Void){
   //...
}

We have three separate functions that each return an array through a closure. This absolutely will work and is a perfectly valid way to handle this. HOWEVER. For my purposes, this doesn’t quite cut it. By getting three different response, we’ll have to update the user interface after each individual response. Again, that’s perfectly fine. But because I’m a pain, I want the user interface to update only when we get a return from all three calls to the Spotify API. So how would we approach this? Well, in theory we could do this:

func query(artist: String, track: String, album: String, completion:([Artist], [Track], [Album]) -> Void){
//...
let task = URLSession.shared.dataTask(with: request, completionHandler: {(data, response, error) in
//...parse to get [Artist]
let task = URLSession.shared.dataTask(with: request, completionHandler: {(data, response, error) in
//...parse to get [Track]
let task = URLSession.shared.dataTask(with: request, completionHandler: {(data, response, error) in
//...parse to get [Album]
completion([Artist], [Track], [Album])

}.resume()
}.resume()
}.resume()
}

Oof. That’s a lot of indentation. And that’s without even getting into parsing through any JSON. And one more thing: because we’re calling each subsequent data task in the completion handler for the previous data task, the data tasks to get [Track] and [Album] are only started once the previous data task has received a response. This isn’t too bad when we have only three calls to the API, but imagine we had to make more calls. Imagine we wanted to search not only for a whole String that the user inputs, but each word in that string. And each possible combination of those words. MORE ITALICIZED THINGS!

The solution is to make each call to the API independent and asynchronous. Then wait until all three calls have returned a response, before delivering it to the UIViewController or whatever else we’re using to display the information.

“But Patrick!” you ask, “How does one accomplish such a thing? Lead me to enlightenment!”

First, as always, I like to point out resources from more experienced developers than myself. Here’s a solid article from Appcoda. It’s a little out of date — NSOperation has been renamed to Operation — but it’s a great summary of what we’re dealing with.

Think of operation queues as a list of tasks to be completed. You can add dependencies to an operation queue, meaning that a list of tasks will only be executed once the list it is dependent on has been completed. So if you, say, made a number of requests to an API, and only wanted to update the user interface once all of those requests have been completed, you can make two Operations. One will make the calls to the API and return the results. The second operation will be dependent on the first operation having been completed. This second operation will then do whatever updates are needed to update the user interface — eg. deliver values to a UIViewController.

The other rad thing about this — and something I learned while writing this article — is that you can also cancel operations within a given queue. I won’t go into that here, but that sounds super useful.

This is all a lot to take in.

Let’s demonstrate how this would be done.

Let’s make an Operation Queue:

let radQueue = OperationQueue()

radQueue has a whole bunch of properties we can set, like qualityOfService and maxConcurrentOperationCount. Those are useful. We need to add operations to our queue:

radQueue.addOperation {
print("operation 1")
}
let operation2 = BlockOperation {
print("operation 2")

}
let operation3 = BlockOperation {
print("operation 3")
}
operation2.addDependency(operation3)
radQueue.addOperation(operation2)
radQueue.addOperation(operation3)

Can you guess what will be printed to the console?

operation 1
operation 3
operation 2

Gnarly. So our third operation is dependent on the execution of operation2 happening before it will execute.

So, in theory, we should be able to put something like this in a function:


let operation1 = BlockOperation {
func query(artist: artist) -> Void){ artists in
//do something with artists
}
func query(track: track) -> Void) {tracks in
//do something with tracks
}
func query(album: album) -> Void){ albums in
//do something with album
}
}
let operation2 = BlockOperation {
print("yay")
// This won't happen until operation1 finishes, so we should be able to do some other stuff like send artists, tracks and albums to update a view controller
}
operation2.addDependency(operation1)

There’s still a problem though. Each of the queries within operation 1 is asynchronous. They fire off, and the operation returns before we’ve gotten any value back from completion closure in each function.

There’s one more tool we can use here, called DispatchGroups. Dispatch groups provide a way to block a thread until a task has been completed.

func coolDispatchFunction() {
let group = DispatchGroup()
   group.enter() 
someAsyncFunction() {result in
//do something with result
group.leave
   }
   group.wait()
//anything after this point won't execute until we get a result from someAsyncFunction()
}

DispatchGroups are extremely useful, and offer an easy way to add dependency to your code. Here’s how we could implement our example from before, using DispatchGroups:

let operation1 = BlockOperation {
   let group = DispatchGroup()
group.enter()
func query(artist: artist) {artists in
//do stuff with artists
group.leave()
}
group.enter()
func query(track: track) { tracks in
//do stuff with tracks
group.leave()
}
group.enter()
func query(album: album) { albums in
//do stuff with albums
group.leave()
   }
group.wait()
//this operation won't return until we have artists, tracks and albums
}
let operation2 = BlockOperation {
print("yay")
// Now, operation2 will fire off once operation1 has completed, with results for artists, tracks and albums
}
operation2.addDependency(operation1)

Now, operation2 will wait until we have results from all three queries before it executes. We can add parameters to the group.wait() function also, such as a timeout value, in case the API we’re calling out to can’t be reached.

Finally, the last important step in all of this. At some point, we’re going to have to update our user interface. When updating the user interface, it’s important to make sure that any changes occur on the main thread. For example, in my app I’m using [Track], [Artist], and [Album] to update a UITableView. After I’ve gotten all of the data I need from the Spotify API, I go back to the main DispatchQueue:

DispatchQueue.main.async {
tableView.reloadData()
}

And there we have it. We now know how to wait for a group of “simultaneous” calls to an API, and perform any updates once all the calls have returned a value. There’s a lot going on here, and many different opinions about the right/wrong way to handle asynchronicity, but I hope I’ve shone a little light on one of the more difficult topics in iOS Development. Next week, we’ll take a look at a cool, third party framework that simplifies a lot of this stuff: PromiseKit. Enjoy your weekend.