Break "for" loop from within async completion handler - swift

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

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

How can I abort the entire dispatch group operation upon a single failure without returning multiple failure completion handlers?

I use dispatch group in a loop in order to keep track an array of network requests so when they are all successfully completed then the successful completion handler is returned only once.
However, if a single failure occur, I want to abort the entire operation and return the failure completion handler only once. The problem I am facing is that all failure completion handlers are returned multiple times. Which is not what I want.
My code looks something like this.
class NetworkClient {
// ...
func fetchBlogPosts(completion: #escaping (Result<[BlogPost], NetworkClientError>) -> Void) {
let dispatchGroup = DispatchGroup()
var blogPosts = [BlogPost]()
for (index, value) in blogPostJSONURLs.enumerated() {
dispatchGroup.enter()
guard let jsonURL = URL(string: blogPostJSONURLs[index]) else {
dispatchGroup.leave()
completion(.failure(.invalidURL))
return
}
let dataTask = URLSession.shared.dataTask(with: jsonURL) { data, response, error in
if error != nil {
dispatchGroup.leave()
completion(.failure(.errorReturned))
return
}
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
dispatchGroup.leave()
completion(.failure(.serverError))
return
}
guard let data = data else {
dispatchGroup.leave()
completion(.failure(.noData))
return
}
do {
let blogPost = try JSONDecoder().decode(BlogPost.self, from: data)
blogPosts.append(blogPost)
dispatchGroup.leave()
} catch {
dispatchGroup.leave()
completion(.failure(.failedToDecodeJSON))
}
}
dataTask.resume()
}
dispatchGroup.notify(queue: .main) {
completion(.success(blogPosts))
}
}
}

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:

How to get notified after looping through Alamofire requests using DispatchQueue in Swift?

I have this code to get a list of ids and names that are parsed from a JSON through an iteration of calls.
Problem is I don't know how to get notified, a simples print("finished"), would do. I tried to use print command after the 'for' loop but it also iterates.
Anyone with any idea?
Here's the code:
override func viewDidLoad() {
super.viewDidLoad()
//Manager
let manager = SessionManager.default.startRequestsImmediately = false
//País
let paisRequest = Alamofire.request(self.cadastro_pais_url, method: .post, parameters: self.cadastro_pais_params).responseString { response in
do { } catch { print("error") }
}
for i in 0...2000 {
DispatchQueue.main.async {
let patrocinadorRequest = Alamofire.request(self.buscaPatrocinador, method: .post, parameters: ["patrocinador":"\(i)"]).responseJSON { (responseData) -> Void in
if((responseData.result.value) != nil) {
let swiftyJsonVar = JSON(responseData.result.value!)
if !(swiftyJsonVar["integracao"] == JSON.null){
print("\(swiftyJsonVar["integracao"]),\(swiftyJsonVar["nome"]),")
} else {}
} else {
print("Error")
}
}
//Requests Chain
let chain = RequestChain(requests: [paisRequest, patrocinadorRequest])
chain.start { (done, error) in
}
}
}
}
The network request should not be done on the main thread, but instead on the background one, sync or async. The main thread is reserved only for the UI stuff, except if you want to force blocking the User interface.
You can use Dispatch Group and DispatchQueue to organise you code and notification after completion. The same result could be achieved with the Semaphore...
Sample:
let dispatchGroup = DispatchGroup()
// change the quality of service based on your needs
let queue = DispatchQueue(label: "com.stackoverflow", qos: .background)
for i in 0...2000 {
dispatchGroup.enter()
// Perform on background thread, async
queue.async {
Alamofire.request { response in
dispatchGroup.leave()
// ...
}
}
}
dispatchGroup.notify(queue: .main, execute: {
print("DONE WITH ALL REQUESTS")
})
Hope it helps.

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