Synchronize multiple asynchronous tasks in Swift
You may face the situations where it is needed some asynchronous tasks to be done first and then you can jump to the next stage of your swift program. In this article we are looking at different solutions we can apply to run the tasks asynchronously and get notified or get the results at the end of the process.
As a common case, you may need to store the results of multiple network requests first and then display the proper data to the user.
Indeed if you have to request many network resources for performing a feature of your application, it may consume more energy of the device and you may think twice about, but it is out of this article’s interest.
Let’s say we have a sample network request function as follows:
func performNetworkRequest(url: String, completion: @escaping (Data?, Error?) -> Void) {
// create a url
let requestUrl = URL(string: url)
// create a data task
let task = URLSession.shared.dataTask(with: requestUrl!) { (data, response, error) in
completion(data, error)
}
task.resume()
}
And there are some network requests that we want to fulfill via the network:
let baseUrl = "https://jsonplaceholder.typicode.com/"
let endpoints = ["posts", "comments", "albums", "photos", "todos", "users"]
In the above example, baseUrl indicates our base endpoint URL and the endpoints are different requests from the web server.
Multiple Independent Tasks (Concurrent Queue)
We can have different async tasks which are independent of each other. It means we can run them in any order and the result of one is not related to the other task.
Grand Central Dispatch or GCD automatically manages the thread creation and balances them based on available device resources in iOS.
We can create a bunch of asynchronous tasks with DispatchGroup and continue our job when all of them is completed:
let group = DispatchGroup()
endpoints.forEach { endpoint in
group.enter()
performNetworkRequest(url: baseUrl + endpoint) { data, error in
print("Task \(endpoint) is done")
group.leave()
}
}
// notify the main thread when all task are completed
group.notify(queue: .main) {
print("All Tasks are done")
}
We have notified the main thread when tasks are done but it could be any other threads. The following output could be the result (or any arbitrary order of completion of sub-tasks):
Task albums is done
Task posts is done
Task todos is done
Task comments is done
Task users is done
Task photos is done
All Tasks are done
As you can see, when all the tasks are processed we received the “All Tasks are done” message.
Promises
There is a pattern which is highly recommended to solve this issue: Promises.
Generally speaking, a promise represents the eventual result of an asynchronous task, respectively the error reason when the task fails. Similar concepts are also called futures (see this wiki article for more information Futures and promises).
We can suppose 3 different states of a promise:
Pending : unresolved task and the result is not yet available
Fulfilled : resolved task with some value (successful response)
Rejected : resolved task with error
There are a bunch of Cocoapods libraries to provide promises. One of the best implementations in Swift is Google Promises:
Now we can update the solution using Promises:
func performNetworkPromise(url: String) -> Promise<Data>{
let promise = Promise<Data>.pending()
performNetworkRequest(url: url) { (data, error) in
guard let data = data else { return }
promise.fulfill(data)
}
return promise
}
func promiseSolution() {
let networkPromises = endpoints.map {
performNetworkPromise(url: baseUrl + $0)
}
all(networkPromises).then { dataArray in
print("All Tasks are done")
}
}
As can be seen, promises ease the async task process. In performNetworkPromise we first created a pending process that some data will be returned. It is like an empty async request container that we have initialized. The type Data in the promise determines what type of data it would be fulfilled with. Then we performed the network request. As a result, when we have a response we fill the promise with the data from that response.
We created an array of network requests in promiseSolution named networkPromises . The all class method waits for all the provided promises , here the network requests, to be fulfilled. Once all of them have been fulfilled with proper data, it will an array of all fulfilled values, [Data] .
The other promise implementation which is so popular in Swift community is PromiseKit.
They are similar to each other (most keywords are the same). You can check it out yourself. Just keep in mind that you should not use both of them in a single scope.
You can find the source code of this article at below repository:
This article is originally published in Medium.
What other ways do you suggest for synchronisation of tasks in iOS?