swiftloopsnsurlsessiondatataskdispatchgroup

dataTask() in a loop: Waiting and breaking


My app downloads a couple of files from a server, using a URLSessionDataTask. When a downloads finishes successfully (and without any errors), then it should start the next download. If there is any type of error, then the whole thing has to abort and display the error message through the calling function. If it finishes without any errors, then it simply switches back to the calling function.

This function is called after another dataTask has finished (using a completion handler) but I never switch back to the main thread, so all of this is still running in the same background thread the previous task used.

My code (Swift 5, Xcode 14.2):

private func fileDownload(fileNames fns:[String]) {
    if !errorBool {
        print("Main thread: \(Thread.isMainThread)")
        let group = DispatchGroup()
        
        myloop:
        for f in fns {
            let url = getURL(f)
            
            group.enter()
            
            //DispatchQueue.global(qos: .background).async {
            let task = session.dataTask(with: url) {(data, response, error) in
                defer { group.leave() }
                print("Starting task!")
                
                if error != nil && data == nil {
                    self.errorBool = true
                    break myloop //TODO 1: "Cannot find label 'myloop' in scope", "self." doesn't help
                }
                
                if let httpResponse = response as? HTTPURLResponse {
                    //Do stuff with downloaded data, more error handling that sets the error flag
                }
            }
            task.resume()
            //}
            
            //TODO 2: How do I wait here for the task to finish?
            //group.wait()
            if errorBool {
                break myloop
            }
        }
        
        group.notify(queue: .main) {
            print("Done!")
            //Displays any errors in a popup (on the main thread) through the calling function
        }
    }
}

There are two things that I'm experiencing problems with:

  1. How do I break the loop from within the task if there's an error ("TODO 1")?
  2. How do I wait at "TODO 2" until the task finishes, so I can break the loop if there are any errors? If I use group.wait() there, then the task never starts (deadlock?), even though it should automatically run on a background thread. I tried to switch to yet another background thread for the task (see inactive code above) but that didn't help either.

Solution

  • I misunderstood how DispatchGroup() works: You can enter the group and leave again once a loop iteration is done but group.wait() waits until everything (docs: "all tasks in the group") is done, not just a single loop iteration, blocking the thread it's currently working on in the process.

    To actually wait for a single task to finish I had to switch to session.data (instead of the "old" session.dataTask):

    private func fileDownload(fileNames fns:[String]) {
        if !errorBool {
            Task {
                var i=0
    
                myloop:
                for f in fns {
                    print("\(i): \(f)")
                    let url = self.getURL(f)
                    
                    await actualFileDownload(url, completion: {(data, response, error) in
                        if error != nil && data == nil {
                            self.errorBool = true
                            //Handle error with custom error message
                            return
                        }
                            
                        if let httpResponse = response as? HTTPURLResponse {
                            //Do stuff with downloaded data, more error handling (error flag & message)
                        }
                    })
    
                    if self.errorBool {
                        break myloop
                    }
                    i+=1
                }
                print("Done!")
            }
        }
        print("End of function!")
    }
    
    private func actualFileDownload(_ url: URL, completion: @escaping (Data?, URLResponse?, Error?) -> Void) async {
        do {
            let (data, response) = try await session.data(from: url) //"await" waits until the download is done
            completion(data, response, nil)
        } catch {
            completion(nil, nil, error) //Completion handler with 3 parameters, like with "dataTask"
        }
    }
    

    This code downloads all files in the correct order and waits for each download to finish before the next one is started.

    The prints will look like this:

    End of function!
    0: testfile1.txt
    1: myimage.jpg
    2: someotherfile.png
    Done!
    

    "End of function!" is printed first because of the Task in fileDownload. The downloads are going to wait for each other but the task won't and code after it will most likely be called before the first download is even done.

    Without the task, the async keyword would be required for the function (and await in the calling function), which would then trickle up the whole function tree! This way this doesn't happen but you have to be careful about what code you want to be called when. If you want to wait for fileDownload too, either use async in combination with await or add a completion handler.