dataTask() in a loop

It seems reasonable for a setting like httpMaximumConnectionsPerHost

Sure. Although I think you need to make your case in an ER rather than here.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

Sorry about the delay, here's the full code:

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
                        return
                    }
                        
                    if let httpResponse = response as? HTTPURLResponse {
                        //Do stuff with downloaded data, more error handling that sets the error flag
                    }
                })

                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 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 (which is what I did).

It feels odd to have an async function that also has a completion handler; one of the primary benefits of async/await is that it eliminates the "out of order" code flow that often comes with completion handlers. Since you want to preserve the non-async nature of the outer function, I would probably write it more like this:

private func fileDownload(fileNames fns:[String]) {
    if !errorBool {
        Task {
            var i=0

            do {
                for f in fns {
                    print("\(i): \(f)")
                    let url = self.getURL(f)
                    let (data, response) = try await actualFileDownload(url)
                    if let httpResponse = response as? HTTPURLResponse {
                        //Do stuff with downloaded data, more error handling that sets the error flag
                    }
                    i+=1
                }
            }
            catch {
                self.errorBool = true
            }
            print("Done!")
        }
    }
    print("End of function!")
}

private func actualFileDownload(_ url: URL) -> Void) async throws -> (Data, URLResponse) {
    let (data, response) = try await session.data(from: url) //"await" waits until the download is done
    return (data, response)
}

This has exactly the same behavior has your version (including bailing after the first error, downloading each file sequentially, printing "End of function" first, etc.), but leverages await to avoid the need for a callback at all on the internal function. And really, you don't even need actualFileDownload(url) at this point, since it just calls straight through to the URL session, but perhaps you're doing other things in there too that weren't shown in the example.

There are other functions in the same class that I created first and that use downloadTask, uploadTask,.... If I switch them to using the session's async functions (instead of the tasks) in the future, then I'll probably just work with the exceptions directly but until then I want to use the completion handler because I want to return the same three parameters (data, response & error) the same way for all download/upload/... functions, so I can use the same code for error handling in all of them. I'm aware that using throws directly is usually the preferred method but in this case it would make maintenance more complicated.