The Issue
I am running into an issue where I have a loop of network calls (called grabImage
), all appending their callback data to the same array, and am correctly using a dispatch group to not leave the function until all network calls are done (until the group's enters/leaves are balanced). However, I have no control over the order in which the callbacks edit the aforementioned array, and am getting random orderings in the data. How do I ensure these callbacks, all occurring in separate threads, run serially to keep an ordering in the global array?
What I’ve tried
I have tried the obvious of using a serial queue, however, because the grabImage
function escapes itself, I think the serial queue might think that it’s done executing before it enters the callback
Relevant Code
//function to grab the uploaded pics of a user and store them
func fetchAllImages(_ userPicArray: [String?], _ completion: @escaping ([UIImage]) -> ()) {
var images: [UIImage] = [] //array these async calls are appending to
for photoLink in userPicArray {
//if the picture (link) exists
if photoLink != nil {
//make sure to append to the array asynchronously
appendImagesSQ.sync { //my attempt to run serially
//grab image and add it to the resImages array
self.grabImage(photoLink!) { (image) in //grabImage itself escapes
self.grabImageDispatchGroup.leave()
images.append(image)
}
}
}
}
grabImageDispatchGroup.notify(queue: grabImageSQ) {
completion(images)
}
}
Making them run sequentially is not the best way to solve this problem. Sure, you’ll get them in order, but it will go much slower than you want. Instead, launch them concurrently, store the results in a dictionary, and then when you’re all done, retrieve the results from the dictionary in order, e.g.
func fetchAllImages(_ userPicArray: [String?], _ completion: @escaping ([UIImage]) -> ()) {
var images: [String: UIImage] = [:] //dictionary these async calls are inserted to
let group = DispatchGroup()
let photoLinks = userPicArray.compactMap { $0 }
for photoLink in photoLinks {
group.enter()
grabImage(photoLink) { image in
images[photoLink] = image
group.leave()
}
}
group.notify(queue: .main) {
let sortedImages = photoLinks.compactMap { images[$0] }
completion(sortedImages)
}
}
By the way, it looks like your grabImage
returns a non-optional. But what if the request failed? You still need to call the completion handler. Make sure grabImage
calls the closure even if no image was retrieved (e.g. make the image property optional and call the closure regardless of success or failure).
Also, does grabImage
call its completion handler on the main queue? If not, you’ll want to make sure it’s called on a serial queue, to ensure thread-safety, or else some synchronization of this images
dictionary will be needed.