Swift Dispatch Group in function - swift

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

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

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:

Swift Async call not updating data right away

I am struggling to get the code below to work in my xcode project.
The first half of the code doesn't actually populate my code until around the cellforrowat function (not in the snippet below). Because of this the second half of the code snipit doesnt even run (because the movie array is still empty).
So I think my problem is that DispatchQueue.main.async {self.tableView.reloadData()} does not update my tableview right away.
var movies: [MovieDT] = []
var db: Firestore
var uid: String = ""
init(genre:Int, categoryNum:[Int], uid:String){
db = Firestore.firestore()
super.init(nibName: nil, bundle: nil)
self.uid = uid
TMDBConfig.apikey = "xxx"
GenresMDB.genre_movies(genreId: categoryNum[genre], include_adult_movies: true, language: "en") { [weak self] apiReturn, movieList in
guard let self = self else {return}
if let movies = movieList
{
for movie in movies
{
self.movies.append(MovieDT(title: movie.title ?? "Missing Title", description: movie.overview ?? " ", releaseDate: movie.release_date ?? " ", stars: movie.vote_average ?? 0, id: movie.id ?? 0))
}
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
//****************************************************************
//code below does not work because the movies aren't generated yet
//****************************************************************
for movie in self.movies
{
self.db.collection("Movies").document(String(movie.id)).getDocument() { [weak self] (querySnapshot, err) in
guard let self = self else {return}
if let err = err {
print("Error getting documents: \(err)")
} else {
guard let snap = querySnapshot else {return}
let data = snap.data()
print(data?["upvoteCount"] as? Int ?? 0)
print(data?["downvoteCount"] as? Int ?? 0)
print("jhsjsj")
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
Since GenresMDB.genre_moves is an asynchronous function, the callback will execute on a separate thread. Meanwhile, init continues executing on the main thread. The API call takes time to complete, so your for loop executes before you've had a chance to update movies. If you add a print right after the guard and another one right before for movie in self.movies, I suspect you'll see the first print after the second one, even though you wrote the first one first.
So, the solution is just to move your for loop into the completion block. I would merge it with your for movie in movies loop. The trick is to make your code execute synchronously and deterministically, to ensure that it will certainly run in the order that you hope.

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

How to set up DispatchGroup in asynchronous iteration?

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