tl;dr I have an OperationQueue and I want to have two operations running at the time. Those operations download something asynchronously hence they all get triggered at once instead of running one after another.
I fill a table of very large images by doing doing the following for each of the images:
public func imageFromUrl(_ urlString: String) {
if let url = NSURL(string: urlString) {
let request = NSURLRequest(url: url as URL)
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: request as URLRequest, completionHandler: {(data, response, error) in
if let imageData = data as Data? {
DispatchQueue.main.async {
self.setImageData(imageData)
}
}
});
task.resume()
}
calling it like imageView.imageFromUrl(...)
.
On slower internet connections, the calls stack and it starts loading every image at once. The user then has to wait for the downloads to "fight" each other and is staring at a blank screen for a while before the images all appear at once (more or less). It would be a much better experience for the user if one image appeared after another.
I thought about queuing up the items, downloading the first of the list, drop it from the list and call the function recursively like this:
func doImageQueue(){
let queueItem = imageQueue[0]
if let url = NSURL(string: (queueItem.url)) {
print("if let url")
let request = NSURLRequest(url: url as URL)
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: request as URLRequest, completionHandler: {(data, response, error) in
print("in")
if let imageData = data as Data? {
print("if let data")
DispatchQueue.main.async {
queueItem.imageView.setImageData(imageData)
self.imageQueue.remove(at: 0)
if(self.imageQueue.count>0) {
self.doImageQueue()
}
}
}
});
task.resume()
}
}
This does load the images one after another, by I think it's a waste of time not to have at least 2 requests running at a time. Making my current implementation handle 2 images at the same time would result in big spaghetti code so I've looked into Swift's OperationQueue
.
I would do
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2
for (all images) {
queue.addOperation {
imageView.imageFromUrl(imageURL
}
}
But this also triggers all the calls at once, probably due to the fact that the requests run asynchronously and the method call ends before waiting for the image to be downloaded. How can I deal with that? The app will also run on watchOS, maybe there is a library for this already but I don't think this should be too hard to achieve without a library. Caching isn't a concern.
Your original code with the operation queue and your original iamgeFromUrl
method are all you need if you make one small change to imageFromUrl
. You need to add a couple lines of code to ensure that imageFromUrl
doesn't return until the download is complete.
This can be done using a semaphore.
public func imageFromUrl(_ urlString: String) {
if let url = URL(string: urlString) {
let request = URLRequest(url: url)
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let semaphore = DispatchSemaphore(value: 0)
let task = session.dataTask(with: request, completionHandler: {(data, response, error) in
semaphore.signal()
if let imageData = data as Data? {
DispatchQueue.main.async {
self.setImageData(imageData)
}
}
});
task.resume()
semaphore.wait()
}
}
As written now, the imageFromUrl
will only return once the download completes. This now allows the operation queue to properly run the 2 desired concurrent operations.
Also note the code is modified to avoid using NSURL
and NSURLRequest
.