How to set up DispatchGroup in asynchronous iteration? - swift

I´m trying to set up an iteration for downloading images. The whole process works, but taking a look in the console´s output, something seems to be wrong.
func download() {
let logos = [Logos]()
let group = DispatchGroup()
logos.forEach { logo in
print("enter")
group.enter()
if logo?.data == nil {
let id = logo?.id as! String
if let checkedUrl = URL(string: "http://www.apple.com/euro/ios/ios8/a/generic/images/\(id).png") {
print(checkedUrl)
LogoRequest.init().downloadImage(url: checkedUrl) { (data) in
logo?.data = data
print("stored")
group.leave()
print("leave")
}
}
}
}
print("loop finished")
}
Output:
enter
http://www.apple.com/euro/ios/ios8/a/generic/images/og.png
enter
http://www.apple.com/euro/ios/ios8/a/generic/images/eg.png
enter
http://www.apple.com/euro/ios/ios8/a/generic/images/sd.png
enter
http://www.apple.com/euro/ios/ios8/a/generic/images/hd.png
loop finished
stored
leave
stored
leave
stored
leave
stored
leave
It looks like the iteration does not care about entering and leaving the DispatchGroup() at all. The webrequests are fired almost at the same time. In my opinion the output should look like this:
enter
http://www.apple.com/euro/ios/ios8/a/generic/images/og.png
stored
leave
enter
http://www.apple.com/euro/ios/ios8/a/generic/images/eg.png
stored
leave
...
loop finished
Did I oversee something? Would be awesome to get some ideas.

What about this:
group.notify(queue: .main) {
print("loop finished")
}
Instead of your normal print.
edit:
func download() {
let logos = [Logos]() // NSManagedObject
let group = DispatchGroup()
logos.forEach { logo in
if logo?.data == nil {
let id = logo?.id as! String
if let checkedUrl = URL(string: "http://www.apple.com/euro/ios/ios8/a/generic/images/\(id).png") {
print(checkedUrl)
print("enter")
group.enter()
LogoRequest.init().downloadImage(url: checkedUrl) { (data) in
//this is async I think
coin?.logo = data
print("stored")
group.leave()
print("leave")
}
}
}
}
group.notify(queue: .main) {
print("loop finished")
}
}

Related

Wait until part of the function completes to execute the function

I'm trying to fetch data and update core data based on the new updated API-Data.
I have this download function:
func download1(stock: String, completion: #escaping (Result<[Quote], NetworkError>) -> Void) {
var internalQuotes = [Quote]()
let downloadQueue = DispatchQueue(label: "com.app.downloadQueue")
let downloadGroup = DispatchGroup()
downloadGroup.enter()
let url = URL(string: API.quoteUrl(for: stock))!
NetworkManager<GlobalQuoteResponse>().fetch(from: url) { (result) in
switch result {
case .failure(let err):
print(err)
downloadQueue.async {
downloadGroup.leave()
}
case .success(let resp):
downloadQueue.async {
internalQuotes.append(resp.quote)
downloadGroup.leave()
}
}
}
downloadGroup.notify(queue: DispatchQueue.global()) {
completion(.success(internalQuotes))
DispatchQueue.main.async {
self.quotes.append(contentsOf: internalQuotes)
}
}
}
On the ContentView I try to implement an update function:
func updateAPI() {
for stock in depot.aktienKatArray {
download.download1(stock: stock.aKat_symbol ?? "") { _ in
//
}
for allS in download.quotes {
if allS.symbol == stock.aKat_symbol {
stock.aKat_currPerShare = Double(allS.price) ?? 0
}
}
}
PersistenceController.shared.saveContext()
}
My problem is that the for loop in the update function should only go on if the first part (download.download1) is finished with downloading the data from the API.
Don't wait! Never wait!
DispatchGroup is a good choice – however nowadays I highly recommend Swift Concurrency – but it's at the wrong place.
.enter() must be called inside the loop before the asynchronous task starts
.leave() must be called exactly once inside the completion handler of the asynchronous task (ensured by a defer statement)
I know this code won't work most likely, but I merged the two functions to the correct DispatchGroup workflow. I removed the custom queue because the NetworkManager is supposed to do its work on a custom background queue
func updateAPI() {
var internalQuotes = [Quote]()
let downloadGroup = DispatchGroup()
for stock in depot.aktienKatArray {
downloadGroup.enter()
let url = URL(string: API.quoteUrl(for: stock))!
NetworkManager<GlobalQuoteResponse>().fetch(from: url) { result in
defer { downloadGroup.leave() }
switch result {
case .failure(let err):
print(err)
case .success(let resp):
internalQuotes.append(resp.quote)
for allS in download.quotes {
if allS.symbol == stock.aKat_symbol {
stock.aKat_currPerShare = Double(allS.price) ?? 0
}
}
}
}
}
downloadGroup.notify(queue: .main) {
self.quotes.append(contentsOf: internalQuotes)
PersistenceController.shared.saveContext()
}
}

Dispatch group don't return fetched data

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)
}
}
}

Swift Dispatch Group in function

thank you in advance. I am working with a UITableView and need an array to be created before loading the cells. I am attempting to use DispatchGroup, I was successful in letting the first array be created but a second array which I also need validPhonesNotUsingApp, in the same function is not created.
I am leaving parts of overall file out.
Thank you.
override func viewDidLoad() {
super.viewDidLoad()
let group = DispatchGroup()
setUpElements()
group.enter()
checkContacts(group)
group.notify(queue: .main){
self.tableView.dataSource = self
self.tableView.delegate = self
self.searchBar.delegate = self
self.tableView.keyboardDismissMode = .onDrag
print(self.validPhonesNotUsingApp)
self.tableView.register(TableViewCellForContacts.nib(), forCellReuseIdentifier: TableViewCellForContacts.identifier)
}
}
func checkContacts(_ group: DispatchGroup){
let db = Firestore.firestore()
db.collection("UserProfile").document(UserDataConst.UserUID).getDocument { (DocumentSnapshot1, Error1) in
if Error1 != nil{
print("Error finding if contacts uploaded")
group.leave()
}
else{
let hasContacts: Bool = DocumentSnapshot1?.get("Contacts Uploaded") as? Bool ?? false
if hasContacts == true{
db.collection("UserProfile").document(UserDataConst.UserUID).collection("ContactFriends").getDocuments { (
Snapshot2, Error2) in
if Error2 != nil{
group.leave()
return
}
else{
for x in 0..<Snapshot2!.documents.count{
group.enter()
let validNumber = self.correctPhoneNumber(Snapshot2!.documents[x].documentID, group)
if validNumber != nil{
self.validPhoneNumbers.append(validNumber!)
let first = Snapshot2!.documents[x].get("First Name") as? String ?? "(ø)"
self.validPhoneFirstName.append(first)
let last = Snapshot2!.documents[x].get("Last Name") as? String ?? "(ø)"
self.validPhoneLastName.append(last)
}
else{
group.leave()
}
}
db.collection("AllPhoneNumbers").getDocuments { (Snapshot3, Error3) in
if Error3 != nil{
group.leave()
return
}
else{
print("OK lemme know what sup")
let docs = Snapshot3!.documents
group.enter()
for x1 in 0..<self.validPhoneNumbers.count{
group.enter()
var found = false
for x2 in 0..<docs.count{
group.enter()
if self.validPhoneNumbers[x1] == docs[x2].documentID{
let uid = docs[x2].get("UID") as! String
db.collection("UserProfile").document(UserDataConst.UserUID).collection("Friends").getDocuments { (QuerySnapshot4, Error4) in
if Error4 != nil{
group.leave()
return
}
else if QuerySnapshot4!.documents.count != 0{
var found2 = false
for x3 in 0..<QuerySnapshot4!.documents.count{
group.enter()
if QuerySnapshot4!.documents[x3].documentID == uid{
found2 = true
//group.leave()
break
}
else{
group.leave()
}
}
if found2 == false{
self.UIDsUsingApp.append(uid)
}
}
else{
self.UIDsUsingApp.append(uid)
}
}
//self.UIDsUsingApp.append(uid)
found = true
//group.leave()
break
}
}
if found == false{
self.validPhonesNotUsingApp.append(self.validPhoneNumbers[x1])
self.validFirstNotUsingApp.append(self.validPhoneFirstName[x1])
self.validLastNotUsingApp.append(self.validPhoneLastName[x1])
group.leave()
}
print("OK now we getting activ")
}
//print(self.UIDsUsingApp)
}
}
}
}
}
else{
group.leave()
return
}
}
}
}
I am working with a UITableView and need an array to be created before loading the cells. I am attempting to use DispatchGroup
Well, don't. You cannot do anything "before loading the cells". Do not intermingle table handling with dispatch. And don't use a dispatch group in that way.
Everything about the table view must be done immediately and on the main queue. You register directly in viewDidLoad on the main queue. You return a cell immediately in cellForRowAt:. You do not "wait" with a dispatch group or in any other manner.
If you have data to gather for the table in a time-consuming way, fine; do that on a background queue, and update your data model, and then reload the table (on the main queue). So:
Initially, if the data is not ready yet, your data source methods find there is no data and they display an empty table.
Later, once you gather your data and tell the table view to reload, your data source methods find there is data and they display it.
A few observations:
You do not want to entangle the “completion handler” logic of checkContacts with dispatch groups you might be using within the function. If you ever find yourself passing dispatch group objects around, that’s generally a sign that you are unnecessarily entangling methods.
So, if you need dispatch group within checkContacts, fine, use that, but don’t encumber the caller with that. Just use the completion handler closure pattern.
Make sure that you are not updating your model objects until the asynchronous process is done.
For example:
func checkContacts(completion: #escaping (Result<[Contact], Error>) -> Void) {
let group = DispatchGroup()
var contacts: [Contact] = [] // in this method, we will only update this local variable
...
group.notify(queue: .main) {
if let error = error {
completion(.failure(error))
} else {
completion(.success(contacts)) // and when we’re done, we’ll report the results
}
}
}
And you’d call it like
checkContacts { results in
switch results {
case .failure(let error):
...
case .success(let contacts):
self.contacts = contacts // only now will we update model properties
... // do whatever UI updates you want, e.g.
self.tableView.reloadData()
}

Working With Async Firebase Calls SwiftUI

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:

Break "for" loop from within async completion handler

My app (Swift 5) sends files to a server, using an async completion handler inside a for loop and i.a. a semaphore to ensure that only a single file is sent at the same time.
If the upload fails or if there's an exception, I want to break the loop to display an error message.
My code:
let group = DispatchGroup()
let queue = DispatchQueue(label: "someLabel")
let sema = DispatchSemaphore(value: 0)
queue.async {
for (i,item) in myArray.enumerated() {
group.enter()
do {
let data = try Data(contentsOf: item.url)
ftpProvider.uploadData(folder: "", filename: item.filename, data: data, multipleFiles: true, completion: { (success, error) in
if success {
print("Upload successful!")
} else {
print("Upload failed!")
//TODO: Break here!
}
group.leave()
sema.signal()
})
sema.wait()
} catch {
print("Error: \(error.localizedDescription)")
//TODO: Break here!
}
}
}
group.notify(queue: queue) {
DispatchQueue.main.async {
print("Done!")
}
}
Adding a break gives me an error message:
Unlabeled 'break' is only allowed inside a loop or switch, a labeled
break is required to exit an if or do
Adding a label to the loop (myLoop: for (i,s) in myArray.enumerated()) doesn't work either:
Use of unresolved label 'myLoop'
break self.myLoop fails too.
Adding a print right before group.enter() proves that the loop isn't simply finishing before the upload of the first file is done, instead the text is printed right before "Upload successful"/"Upload failed" is (as it's supposed to). Because of this breaking should be possible:
How do I break the loop, so I can display an error dialog from within group.notify?
A simple solution without using recursion: Add a Bool to check if the loop should break, then break it outside the completion handler:
let group = DispatchGroup()
let queue = DispatchQueue(label: "someLabel")
let sema = DispatchSemaphore(value: 0)
queue.async {
var everythingOkay:Bool = true
for (i,item) in myArray.enumerated() {
//print("Loop iteration: \(i)")
if everythingOkay {
group.enter()
do {
let data = try Data(contentsOf: item.url)
ftpProvider.uploadData(folder: "", filename: item.filename, data: data, multipleFiles: true, completion: { (success, error) in
if success {
print("Upload successful!")
everythingOkay = true
} else {
print("Upload failed!")
everythingOkay = false
}
group.leave()
sema.signal()
})
sema.wait()
} catch {
print("Error: \(error.localizedDescription)")
everythingOkay = false
}
} else {
break
}
}
}
group.notify(queue: queue) {
DispatchQueue.main.async {
print("Done!")
}
}
Usually using a Bool like this wouldn't work because the loop would finish before the first file is even uploaded.
This is where the DispatchGroup and DispatchSemaphore come into play: They ensure that the next loop iteration isn't started until the previous has finished, which means that the files are going to be uploaded in the order they are listed in myArray (this approach was suggested here).
This can be tested with the print in the above code, which is then going to be printed right before "Upload successful!"/"Upload failed!" for every iteration, e.g.:
Loop iteration: 0
Upload successful
Loop iteration: 1
Upload successful
Loop iteration: 2
Upload failed
Done!
My suggested approach is based on AsynchronousOperation provided in the accepted answer of this question.
Create the class, copy the code and create also a subclass of AsynchronousOperation including your asynchronous task and a completion handler
class FTPOperation: AsynchronousOperation {
var completion : ((Result<Bool,Error>) -> Void)?
let item : Item // replace Item with your custom class
init(item : Item) {
self.item = item
}
override func main() {
do {
let data = try Data(contentsOf: item.url)
ftpProvider.uploadData(folder: "", filename: item.filename, data: data, multipleFiles: true) { (success, error) in
if success {
completion?(.success(true))
} else {
completion?(.failure(error))
}
self.finish()
}
} catch {
completion?(.failure(error))
self.finish()
}
}
}
In the controller add a serial operation queue
let operationQueue : OperationQueue = {
let queue = OperationQueue()
queue.name = "FTPQueue"
queue.maxConcurrentOperationCount = 1
return queue
}()
and run the operations. If an error is returned cancel all pending operations
for item in myArray {
let operation = FTPOperation(item: item)
operation.completion = { result in
switch result {
case .success(_) : print("OK", item.filename)
case .failure(let error) :
print(error)
self.operationQueue.cancelAllOperations()
}
}
operationQueue.addOperation(operation)
}
Add a print line in the finish() method of AsynchronousOperation to prove it