1

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?

Neph
  • 1,823
  • 2
  • 31
  • 69
  • A similar issue can be found here: https://www.reddit.com/r/swift/comments/bih0zn/how_to_break_a_for_loop_from_inside_a_closure/ – swift-lynx Dec 06 '19 at 11:29
  • Rather than the ugly semaphore (the group is pointless anyway in your case) use an Asynchronous Operation and a serial OperationQueue. The benefit is you can cancel **all** operations if a file couldn't be uploaded. – vadian Dec 06 '19 at 11:29
  • @swiftlynx The code you linked to is using recursion too (as in Sh_Khan's answer), which would mean restructuring the whole thing, as there's more to it than I showed in the question. I'd prefer breaking out of the loop instead. – Neph Dec 06 '19 at 12:08
  • @vadian Getting rid of the group and also `group.notify(queue: queue)` calls `print("Done!")` pretty much instantly, before the first file is even uploaded. The code is already running in a background thread. I'm going to look into `OperationQueue`s, okay. In the meantime: Is it even possible to break out of the loop with my current code? – Neph Dec 06 '19 at 12:19
  • @Neph No, it's not. – vadian Dec 06 '19 at 12:54
  • @vadian I found a solution. Fell free to post your approach too though. ;) – Neph Dec 06 '19 at 13:30

2 Answers2

0

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!
Neph
  • 1,823
  • 2
  • 31
  • 69
0

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

vadian
  • 274,689
  • 30
  • 353
  • 361