I have created a method to fetch user messages from Firebase, however when leaving DispatchGroup app crashes leading to this error Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
I'm not sure what I'm doing wrong. Please help and explain me.
Code:
public func fetchMessages(for userId: String, completion: #escaping (_ result: Result<([Message], [String: Message]), Error>) -> Void) {
let group = DispatchGroup()
var messages = [Message]()
var messagesDict = [String: Message]()
group.enter()
database.child("user-messages").child(userId).observe(.childAdded, with: { [weak self] snapshot in
let messageId = snapshot.key
let messagesRef = self?.database.child("messages").child(messageId)
messagesRef?.observeSingleEvent(of: .value, with: { snapshot in
if let dict = snapshot.value as? [String: AnyObject] {
let message = Message(dict: dict)
if let chatPartnerId = message.chatPartnerId() {
messagesDict[chatPartnerId] = message
messages = Array(messagesDict.values)
messages.sort { message1, message2 in
guard let timestamp1 = message1.timestamp?.intValue, let timestamp2 = message2.timestamp?.intValue else { return false }
return timestamp1 > timestamp2
}
group.leave() // Crashes
}
}
}, withCancel: nil)
}, withCancel: nil)
group.notify(queue: .main) {
print("Array: \(messages)\nDict: \(messagesDict)")
}
}
It is because you are using observe option. Which probably is notifying you several times. The crash happens due to the fact that you call ‘leave’ with calling ‘enter’ before. I mean you did call ‘enter’. And you do call ‘leave’. But because you observe the completion is probably called more than once. Which will trigger another ‘leave ‘ call while you called the ‘enter’ only once.
You can easily reproduce the crash with this code
import Foundation
let group = DispatchGroup()
group.enter()
group.notify(queue: .main) {
print("hello")
}
group.leave()
group.leave() // error: Execution was interrupted, reason: EXC_BREAKPOINT (code=1, subcode=0x18013d990).
I'm not sure if you need the observe functionality - i.e listen for changes on the object.
Generally, I'd recommend using Firestore - the new (not that new) DB by Firebase.
Or follow this guide for getting/setting data from Firebase Realtime Database https://firebase.google.com/docs/database/ios/read-and-write
If you need the "listening" i.e observe feature, I'm not sure how the usage of DispatchGroup helps with your implementation. You would generally use it when you, for example, release 2 (or more) API calls in parallel and want to gather all the information from them.
You would create a DispatfchGroup, call enter according to the number of calls you are releasiong, and call leave after you gathered the relevant information.
Something like this
struct AppInfo {
var items: [items] = []
var instructors: [Instructors] = []
var students: [Student] = []
}
func appBootstrapAPICalls(completion: ((AppInfo) -> Void)?) {
var appInfo = AppIfno()
let group = DispatchGroup()
group.enter()
group.enter()
group.enter()
self.fetchItems { items in
appInfo.items.append(contentsOf: items)
group.leave()
}
self.fetchStudents { students in
appInfo.students.append(contentsOf: students)
group.leave()
}
self.fetchInstructors { instructors in
appInfo.instructors.append(contentsOf: instructors)
group.leave()
}
group.notify(queue: .main) {
completion?(appInfo)
}
}
each time i call this api https://foodish-api.herokuapp.com/api/ i get an image. I don't want one image, i need 11 of them, so i made the loop to get 11 images.
But what i can't do is reloading the collection view once the loop is finish.
func loadImages() {
DispatchQueue.main.async {
for _ in 1...11{
let url = URL(string: "https://foodish-api.herokuapp.com/api/")!
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
guard let data = data else { return }
do {
let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String : String]
print(json!["image"]!)
self.namesOfimages.append(json!["image"]!)
} catch {
print("JSON error: \(error.localizedDescription)")
}
}.resume()
}
}
self.collectionV.reloadData()
print("after resume")
}
Typically, when we want to know when a series of concurrent tasks (such as these network requests) are done, we would reach for a DispatchGroup. Call enter before the network request, call leave in the completion handler, and specify a notify block, e.g.
/// Load images
///
/// - Parameter completion: Completion handler to return array of URLs. Called on main queue
func loadImages(completion: #escaping ([URL]) -> Void) {
var imageURLs: [Int: URL] = [:] // note, storing results in local variable, avoiding need to synchronize with property
let group = DispatchGroup()
let count = 11
for index in 0..<count {
let url = URL(string: "https://foodish-api.herokuapp.com/api/")!
group.enter()
URLSession.shared.dataTask(with: url) { data, response, error in
defer { group.leave() }
guard let data = data else { return }
do {
let foodImage = try JSONDecoder().decode(FoodImage.self, from: data)
imageURLs[index] = foodImage.url
} catch {
print("JSON error: \(error.localizedDescription)")
}
}.resume()
}
group.notify(queue: .main) {
let sortedURLs = (0..<count).compactMap { imageURLs[$0] }
completion(sortedURLs)
}
}
Personally, rather than JSONSerialization, I use JSONDecoder with a Decodable type to parse the JSON response. (Also, I find the key name, image, to be a bit misleading, so I renamed it to url to avoid confusion, to make it clear it is a URL for the image, not the image itself.) Thus:
struct FoodImage: Decodable {
let url: URL
enum CodingKeys: String, CodingKey {
case url = "image"
}
}
Also note that the above is not updating properties or reloading the collection view. A routine that is performing network requests should not also be updating the model or the UI. I would leave this in the hands of the caller, e.g.,
var imageURLs: [URL]?
override func viewDidLoad() {
super.viewDidLoad()
// caller will update model and UI
loadImages { [weak self] imageURLs in
self?.imageURLs = imageURLs
self?.collectionView.reloadData()
}
}
Note:
The DispatchQueue.main.async is not necessary. These requests already run asynchronously.
Store the temporary results in a local variable. (And because URLSession uses a serial queue, we do not have to worry about further synchronization.)
The dispatch group notify block, though, uses the .main queue, so that the caller can conveniently update properties and UI directly.
Probably obvious, but I am parsing the URL directly, rather than parsing a string and converting that to a URL.
When fetching results concurrently, you have no assurances regarding the order in which they will complete. So, one will often capture the results in some order-independent structure (such as a dictionary) and then sort the results before passing it back.
In this particular case, the order doesn't strictly matter, but I included this sort-before-return pattern in my above example, as it is generally the desired behavior.
Anyway, that yields:
If you want to get one reload after finish loading of all 11 images you need to use DispatchGroup. Add a property that create a group:
private let group = DispatchGroup()
Then modify your loadImages() function:
func loadImages() {
for _ in 1...11 {
let url = URL(string: "https://foodish-api.herokuapp.com/api/")!
group.enter()
URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
guard let self = self else { return }
self.group.leave()
guard let data = data else { return }
do {
let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String : String]
print(json!["image"]!)
self.namesOfimages.append(json!["image"]!)
} catch {
print("JSON error: \(error.localizedDescription)")
}
}.resume()
}
group.notify(queue: .main) { [weak self] in
self?.collectionV.reloadData()
}
}
Some description:
On the method call group.enter() will be called 11 times
On each completion of image downloading group.leave() will be called
When group.leave() will be called the same count like group.enter() group make call of the block that you defined in group.notify()
More about DispatchGroup
Notice that you need handle create and store different DispatchGroup object if you need to download different groups of images in the same time.
I'm trying to use DispatchGroup for fetching data from multiple request.
I cant understand why print(weatherData.fact.pressureMm!) is working, but data didn't appending inside dataArray and print(dataArray?[0].fact.pressureMm ?? "nil") print nil.
Also i'm try print data from complitionHandeler and result was same.
How i can append weatherData inside array and get value from complition correctly?
func fetchWeatherForCities (complitionHandeler: #escaping([YandexWeatherData]?)->Void) {
var dataArray: [YandexWeatherData]?
let group = DispatchGroup()
for city in cities {
group.enter()
DispatchQueue.global().async {
var urlString = self.urlString
self.locationManager.getCoordinate(forCity: city) { (coordinate) in
urlString += self.latitudeField + coordinate.latitude
urlString += self.longitudeField + coordinate.longitude
guard let url = URL(string: urlString) else {return}
var request = URLRequest(url: url)
request.addValue(self.apiKey, forHTTPHeaderField: self.apiField)
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
print(error)
}
if let data = data {
guard let weatherData = self.parseJSON(withData: data) else {return}
print(weatherData.fact.pressureMm!)
dataArray?.append(weatherData)
print(dataArray?[0].fact.pressureMm ?? "nil")
group.leave()
}
}
dataTask.resume()
}
}
}
group.notify(queue: DispatchQueue.global()) {
complitionHandeler(dataArray)
}
}
A few issues:
You have paths of execution where, if an error occurred, you would not call leave. Make sure every path of execution, including every “early exit”, offsets the enter with a leave.
You defined dataArray to be an optional, but never initialize it. Thus it is nil. And dataArray?.append(weatherData) therefore will never append values.
Thus, perhaps:
func fetchWeatherForCities (completionHandler: #escaping ([YandexWeatherData]) -> Void) {
var dataArray: [YandexWeatherData] = []
let group = DispatchGroup()
for city in cities {
group.enter()
var urlString = self.urlString
self.locationManager.getCoordinate(forCity: city) { (coordinate) in
urlString += self.latitudeField + coordinate.latitude
urlString += self.longitudeField + coordinate.longitude
guard let url = URL(string: urlString) else {
group.leave() // make sure to `leave` in early exit
return
}
var request = URLRequest(url: url)
request.addValue(self.apiKey, forHTTPHeaderField: self.apiField)
let dataTask = URLSession.shared.dataTask(with: request) { data, response, error in
guard
let data = data,
error == nil,
let weatherData = self.parseJSON(withData: data)
else {
group.leave() // make sure to `leave` in early exit
print(error ?? "unknown error")
return
}
print(weatherData.fact.pressureMm!) // I'd advise against every doing force unwrapping on results from a third party service
dataArray.append(weatherData)
group.leave()
}
dataTask.resume()
}
}
group.notify(queue: .main) {
completionHandler(dataArray)
}
}
As an aside, in the above, I have made two unrelated GCD changes, namely:
Removed the dispatching of the network request to a global queue. Network requests are already asynchronous, so dispatching the creation of the request and the starting of that request is a bit redundant.
In your notify block, you were using a global queue. You certainly can do that if you really need, but most likely you are going to be updating model objects (which requires synchronization if you're doing that from a background queue) and UI updates. Life is easier if you just dispatch that to the main queue.
FWIW, when you get past your current issue, you may want to consider two other things:
If retrieving details for many locations, you might want to constrain this to only run a certain number of requests at a time (and avoid timeouts on the latter ones). One way is to use a non-zero semaphore:
DispatchQueue.global().async {
let semaphore = DispatchSemaphore(value: 4)
for i in ... {
semaphore.wait()
someAsynchronousProcess(...) {
...
semaphore.signal()
}
}
}
If you have used semaphores in the past, this might feel backwards (waiting before signaling; lol), but the non-zero semaphore will let four of them start, and others will start as the prior four individually finish/signal.
Also, because we are now waiting, we have to re-introduce the dispatch to a background queue to avoid blocking.
When running asynchronous requests concurrently, they may not finish in the order that you started them. If you want them in the same order, one solution is to store the results in a dictionary as they finish, and in the notify block, build a sorted array of the results:
var results: [Int: Foo] = [:]
// start all the requests, populating a dictionary with the results
for (index, city) in cities.enumerated() {
group.enter()
someAsynchronousProcess { foo in
results[i] = foo
group.leave()
}
}
// when all done, build an array in the desired order
group.notify(queue: .main) {
let array = self.cities.indices.map { results[$0] } // build sorted array of `[Foo?]`
completionHandler(array)
}
That begs the question about how you want to handle errors, so you might make it an array of optionals (like shown below).
Pulling that together, perhaps:
func fetchWeatherForCities(completionHandler: #escaping ([YandexWeatherData?]) -> Void) {
DispatchQueue.global().async {
var results: [Int: YandexWeatherData] = [:]
let semaphore = DispatchSemaphore(value: 4)
let group = DispatchGroup()
for (index, city) in self.cities.enumerated() {
group.enter()
semaphore.wait()
var urlString = self.urlString
self.locationManager.getCoordinate(forCity: city) { coordinate in
urlString += self.latitudeField + coordinate.latitude
urlString += self.longitudeField + coordinate.longitude
guard let url = URL(string: urlString) else {
semaphore.signal()
group.leave() // make sure to `leave` in early exit
return
}
var request = URLRequest(url: url)
request.addValue(self.apiKey, forHTTPHeaderField: self.apiField)
let dataTask = URLSession.shared.dataTask(with: request) { data, response, error in
defer {
semaphore.signal()
group.leave() // make sure to `leave`, whether successful or not
}
guard
let data = data,
error == nil,
let weatherData = self.parseJSON(withData: data)
else {
print(error ?? "unknown error")
return
}
results[index] = weatherData
}
dataTask.resume()
}
}
group.notify(queue: .main) {
let array = self.cities.indices.map { results[$0] } // build sorted array
completionHandler(array)
}
}
}
I understand that the Firebase getDocument call is Async, so I'm trying to figure out how to essentially wait until the call finishes executing, and then move on to doing other stuff.
I have tried making use of DispatchGroup() and entering/leaving the group, but I can't seem to get it to work correctly. I have something like the following:
let myGroup = DispatchGroup()
let usersRef = self.db.collection("Users").document("Users").collection("Users")
if self.testCondition == false {
self.errorMessage = "error"
} else{
usersRef.getDocuments {(snap, err) in
myGroup.enter()
//basically getting every username
for document in snap!.documents{
let user = document["username"] as! String
let userRef = usersRef.document(user)
userRef.getDocument { (snapshot, err) in
if err != nil {
print(err)
} else {
let sample = snapshot!["sample"] as! String
if sample == 'bad' {
self.errorMessage = "error"
}
}
}
}
myGroup.leave()
}
print("what4")
//I would like it so that I can execute everything in a code block like this
//after the async call finishes
myGroup.notify(queue: .main) {
print("Finished all requests.")
//THEN DO MORE STUFF
}
}
How can I modify the placement myGroup.enter() and myGroup.leave() in this so that, after the Firebase call has finished, I can continue executing code?
Thanks!
This explains the DispatchGroup() a little bit.
You just have one litte mistake in your code then it should be working.
Make sure to enter() the group outside of the Firebase getDocuments() call. As this already makes the request and takes time thus the process will continue.
This little simple example should help you understand it:
func dispatchGroupExample() {
// Initialize the DispatchGroup
let group = DispatchGroup()
print("starting")
// Enter the group outside of the getDocuments call
group.enter()
let db = Firestore.firestore()
let docRef = db.collection("test")
docRef.getDocuments { (snapshots, error) in
if let documents = snapshots?.documents {
for doc in documents {
print(doc["name"])
}
}
// leave the group when done
group.leave()
}
// Continue in here when done above
group.notify(queue: DispatchQueue.global(qos: .background)) {
print("all names returned, we can continue")
}
}
When waiting for multiple asynchronous calls use completing in the asynchronous function which you let return as soon as you leave the group. Full eg. below:
class Test {
init() {
self.twoNestedAsync()
}
func twoNestedAsync() {
let group = DispatchGroup() // Init DispatchGroup
// First Enter
group.enter()
print("calling first asynch")
self.dispatchGroupExample() { isSucceeded in
// Only leave when dispatchGroup returns the escaping bool
if isSucceeded {
group.leave()
} else {
// returned false
group.leave()
}
}
// Enter second
group.enter()
print("calling second asynch")
self.waitAndReturn(){ isSucceeded in
// Only return once the escaping bool comes back
if isSucceeded {
group.leave()
} else {
//returned false
group.leave()
}
}
group.notify(queue: .main) {
print("all asynch done")
}
}
// Now added escaping bool which gets returned when done
func dispatchGroupExample(completing: #escaping (Bool) -> Void) {
// Initialize the DispatchGroup
let group = DispatchGroup()
print("starting")
// Enter the group outside of the getDocuments call
group.enter()
let db = Firestore.firestore()
let docRef = db.collection("test")
docRef.getDocuments { (snapshots, error) in
if let documents = snapshots?.documents {
for doc in documents {
print(doc["name"])
}
// leave the group when succesful and done
group.leave()
}
if let error = error {
// make sure to handle this
completing(false)
group.leave()
}
}
// Continue in here when done above
group.notify(queue: DispatchQueue.global(qos: .background)) {
print("all names returned, we can continue")
//send escaping bool.
completing(true)
}
}
func waitAndReturn(completing: #escaping (Bool) -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2), execute: {
print("Done waiting for 2 seconds")
completing(true)
})
}
}
This gives us the following output:
I would like a for in loop to send off a bunch of network requests to firebase, then pass the data to a new view controller once the the method finishes executing. Here is my code:
var datesArray = [String: AnyObject]()
for key in locationsArray {
let ref = Firebase(url: "http://myfirebase.com/" + "\(key.0)")
ref.observeSingleEventOfType(.Value, withBlock: { snapshot in
datesArray["\(key.0)"] = snapshot.value
})
}
// Segue to new view controller here and pass datesArray once it is complete
I have a couple concerns. First, how do I wait until the for loop is finished and all the network requests are complete? I can't modify the observeSingleEventOfType function, it is part of the firebase SDK. Also, will I create some sort of race condition by trying to access the datesArray from different iterations of the for loop (hope that makes sense)? I've been reading about GCD and NSOperation but I'm a bit lost as this is the first app I've built.
Note: Locations array is an array containing the keys I need to access in firebase. Also, it's important that the network requests are fired off asynchronously. I just want to wait until ALL the asynchronous requests complete before I pass the datesArray to the next view controller.
You can use dispatch groups to fire an asynchronous callback when all your requests finish.
Here's an example using dispatch groups to execute a callback asynchronously when multiple networking requests have all finished.
override func viewDidLoad() {
super.viewDidLoad()
let myGroup = DispatchGroup()
for i in 0 ..< 5 {
myGroup.enter()
Alamofire.request("https://httpbin.org/get", parameters: ["foo": "bar"]).responseJSON { response in
print("Finished request \(i)")
myGroup.leave()
}
}
myGroup.notify(queue: .main) {
print("Finished all requests.")
}
}
Output
Finished request 1
Finished request 0
Finished request 2
Finished request 3
Finished request 4
Finished all requests.
Swift 3 or 4
If you don't care about orders, use #paulvs's answer, it works perfectly.
else just in case if anyone wants to get the result in order instead of fire them concurrently, here is the code.
let dispatchGroup = DispatchGroup()
let dispatchQueue = DispatchQueue(label: "any-label-name")
let dispatchSemaphore = DispatchSemaphore(value: 0)
dispatchQueue.async {
// use array categories as an example.
for c in self.categories {
if let id = c.categoryId {
dispatchGroup.enter()
self.downloadProductsByCategory(categoryId: id) { success, data in
if success, let products = data {
self.products.append(products)
}
dispatchSemaphore.signal()
dispatchGroup.leave()
}
dispatchSemaphore.wait()
}
}
}
dispatchGroup.notify(queue: dispatchQueue) {
DispatchQueue.main.async {
self.refreshOrderTable { _ in
self.productCollectionView.reloadData()
}
}
}
Xcode 8.3.1 - Swift 3
This is the accepted answer of paulvs, converted to Swift 3:
let myGroup = DispatchGroup()
override func viewDidLoad() {
super.viewDidLoad()
for i in 0 ..< 5 {
myGroup.enter()
Alamofire.request(.GET, "https://httpbin.org/get", parameters: ["foo": "bar"]).responseJSON { response in
print("Finished request \(i)")
myGroup.leave()
}
}
myGroup.notify(queue: DispatchQueue.main, execute: {
print("Finished all requests.")
})
}
Details
Xcode 10.2.1 (10E1001), Swift 5
Solution
import Foundation
class SimultaneousOperationsQueue {
typealias CompleteClosure = ()->()
private let dispatchQueue: DispatchQueue
private lazy var tasksCompletionQueue = DispatchQueue.main
private let semaphore: DispatchSemaphore
var whenCompleteAll: (()->())?
private lazy var numberOfPendingActionsSemaphore = DispatchSemaphore(value: 1)
private lazy var _numberOfPendingActions = 0
var numberOfPendingTasks: Int {
get {
numberOfPendingActionsSemaphore.wait()
defer { numberOfPendingActionsSemaphore.signal() }
return _numberOfPendingActions
}
set(value) {
numberOfPendingActionsSemaphore.wait()
defer { numberOfPendingActionsSemaphore.signal() }
_numberOfPendingActions = value
}
}
init(numberOfSimultaneousActions: Int, dispatchQueueLabel: String) {
dispatchQueue = DispatchQueue(label: dispatchQueueLabel)
semaphore = DispatchSemaphore(value: numberOfSimultaneousActions)
}
func run(closure: ((#escaping CompleteClosure) -> Void)?) {
numberOfPendingTasks += 1
dispatchQueue.async { [weak self] in
guard let self = self,
let closure = closure else { return }
self.semaphore.wait()
closure {
defer { self.semaphore.signal() }
self.numberOfPendingTasks -= 1
if self.numberOfPendingTasks == 0, let closure = self.whenCompleteAll {
self.tasksCompletionQueue.async { closure() }
}
}
}
}
func run(closure: (() -> Void)?) {
numberOfPendingTasks += 1
dispatchQueue.async { [weak self] in
guard let self = self,
let closure = closure else { return }
self.semaphore.wait(); defer { self.semaphore.signal() }
closure()
self.numberOfPendingTasks -= 1
if self.numberOfPendingTasks == 0, let closure = self.whenCompleteAll {
self.tasksCompletionQueue.async { closure() }
}
}
}
}
Usage
let queue = SimultaneousOperationsQueue(numberOfSimultaneousActions: 1, dispatchQueueLabel: "AnyString")
queue.whenCompleteAll = { print("All Done") }
// add task with sync/async code
queue.run { completeClosure in
// your code here...
// Make signal that this closure finished
completeClosure()
}
// add task only with sync code
queue.run {
// your code here...
}
Full sample
import UIKit
class ViewController: UIViewController {
private lazy var queue = { SimultaneousOperationsQueue(numberOfSimultaneousActions: 1,
dispatchQueueLabel: "AnyString") }()
private weak var button: UIButton!
private weak var label: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton(frame: CGRect(x: 50, y: 80, width: 100, height: 100))
button.setTitleColor(.blue, for: .normal)
button.titleLabel?.numberOfLines = 0
view.addSubview(button)
self.button = button
let label = UILabel(frame: CGRect(x: 180, y: 50, width: 100, height: 100))
label.text = ""
label.numberOfLines = 0
label.textAlignment = .natural
view.addSubview(label)
self.label = label
queue.whenCompleteAll = { [weak self] in self?.label.text = "All tasks completed" }
//sample1()
sample2()
}
func sample1() {
button.setTitle("Run 2 task", for: .normal)
button.addTarget(self, action: #selector(sample1Action), for: .touchUpInside)
}
func sample2() {
button.setTitle("Run 10 tasks", for: .normal)
button.addTarget(self, action: #selector(sample2Action), for: .touchUpInside)
}
private func add2Tasks() {
queue.run { completeTask in
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + .seconds(1)) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.label.text = "pending tasks \(self.queue.numberOfPendingTasks)"
}
completeTask()
}
}
queue.run {
sleep(1)
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.label.text = "pending tasks \(self.queue.numberOfPendingTasks)"
}
}
}
#objc func sample1Action() {
label.text = "pending tasks \(queue.numberOfPendingTasks)"
add2Tasks()
}
#objc func sample2Action() {
label.text = "pending tasks \(queue.numberOfPendingTasks)"
for _ in 0..<5 { add2Tasks() }
}
}
Update for iOS 15+ (Swift 5.5)
I've added a more modern solution for Swift 5.5 and iOS 15+ because this toolchain includes major URLSession API improvements, that are not specific to Firebase or Alamofire. The code uses async / await i.e. Structured Concurrency. It's what Apple recommends for concurrent requests on the latest iOS versions (iOS 13.0+).
We now achieve the same result as DispatchGroups with fewer lines of code and more customisation. This answer will help users who used to queue URLSession requests and wait for these to complete.
Task group example code
The right tool is a TaskGroup if we have a dynamic number of requests (variable-sized array).
func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
var thumbnails: [String: UIImage] = [:]
try await withThrowingTaskGroup(of: (String, UIImage).self) { group in
for id in ids {
group.addTask {
return (id, try await fetchOneThumbnail(withID: id))
}
}
for try await (id, thumbnail) in group {
thumbnails[id] = thumbnail
}
}
return thumbnails
}
func fetchOneThumbnail(withID id: String) async throws -> UIImage {
// Just for demo purpose. In PROD, we may use dynamic URLs for each ID.
guard let url = URL(string: "http://placekitten.com/200/300") else {
throw ThumbnailError.invalidURL
}
// I have used `data(from: URL, delegate: URLSessionTaskDelegate? = nil)`
// but we can also use `data(for: URLRequest, delegate: URLSessionTaskDelegate? = nil)`)`.
// If we want to observe the delegate changes like when the
// request fails, completes, or redirects, use the delegate param.
// e.g. try await URLSession.shared.data(from: url, delegate: delegate)
let result: (data: Data, response: URLResponse) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: result.data) else {
throw ThumbnailError.missingImageData
}
return image
}
enum ThumbnailError: Error {
case invalidURL
case missingImageData
}
Task {
let images = try await fetchThumbnails(for: ["1", "2", "3"])
// Show thumbnails in UI.
}
This also uses the for await loop (AsyncSequence) to wait for tasks to complete. for try await is an example of a throwing AsyncSequence. The throwing syntax is because the new asynchronous URLSession.data(for:) family of methods are throwing functions.
async let example code
async let syntax works for a fixed number of requests.
let reqOne = urlRequest(for: keyOne) // Function that returns a unique URLRequest object for this key. i.e. different URLs or format.
async let (dataOne, _) = URLSession.shared.data(for: reqOne)
let reqTwo = urlRequest(for: keyTwo)
async let (dataTwo, _) = URLSession.shared.data(for: reqTwo)
guard let parsedData = parseInformation(from: try? await dataOne) else {
// Call function to parse image, text or content from data.
continue
}
// Act on parsed data if needed.
guard let parsedDataTwo = parseInformation(from: try? await dataTwo) else {
// Call function to parse image, text or content from data.
continue
}
// Act on the second requests parsed data if needed.
// Here, we know that the queued requests have all completed.
The syntax where I don't await for the request to finish immediately is called async let.
This code example could be adapted with variable-sized arrays but isn't recommended by Apple. This is because async let doesn't always allow the requests to be processed as soon as they arrive.
The benefits of this approach are cleaner code that's easier to write, safer, and avoiding deadlocks/threading issues.
Note
The exact syntax of TaskGroup and async let may change in the future. Currently, Structured Concurrency has improved a lot during its early releases and is now stable for production.
Apple has clarified that the underlying mechanics of grouped and asynchronous tasks are mostly finalised (approved in Swift Evolution). An example of some syntax changes already includes the replacement of async { with Task {.
You will need to use semaphores for this purpose.
//Create the semaphore with count equal to the number of requests that will be made.
let semaphore = dispatch_semaphore_create(locationsArray.count)
for key in locationsArray {
let ref = Firebase(url: "http://myfirebase.com/" + "\(key.0)")
ref.observeSingleEventOfType(.Value, withBlock: { snapshot in
datesArray["\(key.0)"] = snapshot.value
//For each request completed, signal the semaphore
dispatch_semaphore_signal(semaphore)
})
}
//Wait on the semaphore until all requests are completed
let timeoutLengthInNanoSeconds: Int64 = 10000000000 //Adjust the timeout to suit your case
let timeout = dispatch_time(DISPATCH_TIME_NOW, timeoutLengthInNanoSeconds)
dispatch_semaphore_wait(semaphore, timeout)
//When you reach here all request would have been completed or timeout would have occurred.
We can do this with recursion.
Get idea from below code :
var count = 0
func uploadImages(){
if count < viewModel.uploadImageModelArray.count {
let item = viewModel.uploadImageModelArray[count]
self.viewModel.uploadImageExpense(filePath: item.imagePath, docType: "image/png", fileName: item.fileName ?? "", title: item.imageName ?? "", notes: item.notes ?? "", location: item.location ?? "") { (status) in
if status ?? false {
// successfully uploaded
}else{
// failed
}
self.count += 1
self.uploadImages()
}
}
}
Swift 3:
You could also use semaphores on this way. It results very helpful, besides you can keep exact track on when and what processes are completed. This has been extracted from my code:
//You have to create your own queue or if you need the Default queue
let persons = persistentContainer.viewContext.persons
print("How many persons on database: \(persons.count())")
let numberOfPersons = persons.count()
for eachPerson in persons{
queuePersonDetail.async {
self.getPersonDetailAndSave(personId: eachPerson.personId){person2, error in
print("Person detail: \(person2?.fullName)")
//When we get the completionHandler we send the signal
semaphorePersonDetailAndSave.signal()
}
}
}
//Here we will wait
for i in 0..<numberOfPersons{
semaphorePersonDetailAndSave.wait()
NSLog("\(i + 1)/\(persons.count()) completed")
}
//And here the flow continues...
In the original question, it was contemplated how to know when all of the queries were done, returning the results in a dictionary. paulvs has answered (+1) that question: If using old completion handler closure pattern, use a dispatch group to know when they’re done. And, nowadays, if using Swift concurrency, use the pattern proposed by Pranav Kasetti.
But, if you need the results in an ordered array, though, one should not make the requests, themselves, run sequentially. You pay a serious performance penalty (often more than 3× slower) if you do that. And if you achieve this through the use semaphores, you introduce all sorts of other inefficiencies and deadlock risks.
Instead, if you really need the results in an ordered array, you should employ paulvs’s answer to populate a dictionary from the concurrent requests, but then build an array of the results at the end. E.g.,
let array = ids.compactMap { resultsDictionary[$0] }
For example, Swift concurrency handles this all very gracefully:
func fetch(for ids: [Id]) async throws -> [Foo] {
try await withThrowingTaskGroup(of: (Id, Foo).self) { [self] group in
for id in ids {
group.addTask { (id, try await fetch(for: id)) }
}
let dictionary = try await group.reduce(into: [:]) { $0[$1.0] = $1.1 }
return ids.compactMap { dictionary[$0] }
}
}
But even if you are using the older completion handler closure pattern the idea is the same: Store your results in a dictionary, enjoy concurrency, and build the sorted array at the end if you really need it.
Dispatch group is good but the order of sent requests is random.
Finished request 1
Finished request 0
Finished request 2
In my project case, each requests needed to be launch is the right order. If this could help someone :
public class RequestItem: NSObject {
public var urlToCall: String = ""
public var method: HTTPMethod = .get
public var params: [String: String] = [:]
public var headers: [String: String] = [:]
}
public func trySendRequestsNotSent (trySendRequestsNotSentCompletionHandler: #escaping ([Error]) -> () = { _ in }) {
// If there is requests
if !requestItemsToSend.isEmpty {
let requestItemsToSendCopy = requestItemsToSend
NSLog("Send list started")
launchRequestsInOrder(requestItemsToSendCopy, 0, [], launchRequestsInOrderCompletionBlock: { index, errors in
trySendRequestsNotSentCompletionHandler(errors)
})
}
else {
trySendRequestsNotSentCompletionHandler([])
}
}
private func launchRequestsInOrder (_ requestItemsToSend: [RequestItem], _ index: Int, _ errors: [Error], launchRequestsInOrderCompletionBlock: #escaping (_ index: Int, _ errors: [Error] ) -> Void) {
executeRequest(requestItemsToSend, index, errors, executeRequestCompletionBlock: { currentIndex, errors in
if currentIndex < requestItemsToSend.count {
// We didn't reach last request, launch next request
self.launchRequestsInOrder(requestItemsToSend, currentIndex, errors, launchRequestsInOrderCompletionBlock: { index, errors in
launchRequestsInOrderCompletionBlock(currentIndex, errors)
})
}
else {
// We parse and send all requests
NSLog("Send list finished")
launchRequestsInOrderCompletionBlock(currentIndex, errors)
}
})
}
private func executeRequest (_ requestItemsToSend: [RequestItem], _ index: Int, _ errors: [Error], executeRequestCompletionBlock: #escaping (_ index: Int, _ errors: [Error]) -> Void) {
NSLog("Send request %d", index)
Alamofire.request(requestItemsToSend[index].urlToCall, method: requestItemsToSend[index].method, parameters: requestItemsToSend[index].params, headers: requestItemsToSend[index].headers).responseJSON { response in
var errors: [Error] = errors
switch response.result {
case .success:
// Request sended successfully, we can remove it from not sended request array
self.requestItemsToSend.remove(at: index)
break
case .failure:
// Still not send we append arror
errors.append(response.result.error!)
break
}
NSLog("Receive request %d", index)
executeRequestCompletionBlock(index+1, errors)
}
}
Call :
trySendRequestsNotSent()
Result :
Send list started
Send request 0
Receive request 0
Send request 1
Receive request 1
Send request 2
Receive request 2
...
Send list finished
See for more infos :
Gist