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:
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.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.