I am downloading images from Unsplash API and want to make sure that while the image is loading the user scrolls the data task for that particular cell that is no longer is dismissed. I am creating a URLSessionDataTask
, and then calling task.cancel()
in prepareToReuse()
method.
I want to know if this is the correct way to cancel an image request? Also, what are alternate ways of achieving the same thing. Since the whole URLSessionDataTask
is an asynchronous operation, I don't really see why I should be wrapping it in a DispatchGroup or DispatchWorkItem
. Any tips on how to make smooth asynchronous image loading without caching. (I have implemented NSCache in the code below).
Below is the ImageDownloader
class:
var cache = NSCache<NSURL,UIImage>()
class ImageDownloader {
var task: URLSessionTask?
func imageDownloader(url: String, completion: @escaping (UIImage?) -> ()) {
guard let imageURL = URL(string: url) else { return }
if let cachedImage = cache.object(forKey: imageURL as NSURL) {
completion(cachedImage)
return
}
task = URLSession.shared.dataTask(with: imageURL, completionHandler: { data, response, err in
if err != nil {
completion(nil)
print(err?.localizedDescription)
return
}
guard let data = data, let image = UIImage(data: data) else {
completion(nil)
return
}
cache.setObject(image, forKey: imageURL as NSURL)
completion(image)
})
task?.resume()
}
func cancelImageRequest() {
task?.cancel()
}
}
Here is how I am calling it in custom CollectionViewCell
:
func configure(image: ImageModel) {
imageDownloader = ImageDownloader()
guard let urlstring = image.urls.full else { return }
imageDownloader?.imageDownloader(url: urlstring, completion: { image in
DispatchQueue.main.async {
self.cellImage.image = image
}
})
}
override func prepareForReuse() {
cellImage.image = nil
imageDownloader?.cancelImageRequest()
}
CodePudding user response:
In according with the documentation task?.cancel()
:
This method returns immediately, marking the task as being canceled. Once a task is marked as being canceled, urlSession(_:task:didCompleteWithError:) will be sent to the task delegate, passing an error in the domain NSURLErrorDomain with the code NSURLErrorCancelled.
So the first answer is true, the correct way in general to cancel a data task is use cancel()
once the task is started.
CodePudding user response:
Correct, just cancel
the URLSessionTask
. No need for DispatchGroup
or DispatchWorkItem
.
Regarding “smooth asynchronous image loading”, other tips include:
Ensure you're downloading assets of an appropriate size for your UI, and if not available server-side, resize them client-side appropriately on a background queue and cache that, not the original asset ... e.g., if you have huge assets and are only showing thumbnails, the large images that are downsampled by UIKit can cause hitches in your UI;
Use prefetching for your table/collection view so the appropriate images are likely downloaded before the cell appears in the UI;
A few other minor observations:
You might want to make sure that your downloader confirms that the cell has not been reused by the time the completion handler is called, preventing showing the wrong image in race conditions. It is an edge-case, but as they say, there are no benign races.
This code snippet is treating every image separately. You may want to have a service that oversees all the requests, to avoid duplicative requests. E.g., if the first ten cells are all using the same image, you are going to trigger the downloading of the same image ten times (because the first request will not have had a chance to update the cache by the time the subsequent cells end up asking for the same image). This is an edge-case, admittedly, but something you might consider.
I would be inclined to make the
URLSessionTask
aweak
property. The only trick there is to use a local variable for theURLSessionTask
inimageDownloader(url:completion:)
,resume
it, and only then set thetask
property to your local variable. That way, the property will automatically be set tonil
when the request finishes. I'd also use[weak self]
capture list in theimageDownloader
completion closure.class ImageDownloader { weak var task: URLSessionTask? func imageDownloader(url: String, completion: @escaping (UIImage?) -> ()) { guard let imageURL = URL(string: url) else { completion(nil) return } if let cachedImage = cache.object(forKey: imageURL as NSURL) { completion(cachedImage) return } let task = URLSession.shared.dataTask(with: imageURL) { data, response, err in if err != nil { completion(nil) print(err?.localizedDescription) return } guard let data = data, let image = UIImage(data: data) else { completion(nil) return } cache.setObject(image, forKey: imageURL as NSURL) completion(image) } task?.resume() self.task = task } func cancelImageRequest() { task?.cancel() } }
Note, make sure that you call the completion handler for all paths of execution (e.g., even if the URL is invalid). You can often introduce bugs that are hard to diagnose if you do not reliably call the completion handler.
It is a stylistic issue, but we would generally make the completion handler parameter a
Result<UIImage, Error>
in case the UI would ever need to change its behavior based upon the reason the image retrieval failed.
There are a lot of little details to get right here. I'd suggest seriously contemplating well-established UIImageView
extensions for asynchronous image retrieval, caching, resizing, etc., such as KingFisher, AlamofireImage, or SDWebImage.