Home > Back-end >  Correct way to cancel a URLSessionDataTask while downloading an image
Correct way to cancel a URLSessionDataTask while downloading an image

Time:10-25

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:

  1. 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;

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

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

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

  3. I would be inclined to make the URLSessionTask a weak property. The only trick there is to use a local variable for the URLSessionTask in imageDownloader(url:completion:), resume it, and only then set the task property to your local variable. That way, the property will automatically be set to nil when the request finishes. I'd also use [weak self] capture list in the imageDownloader 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()
        }
    }
    
    
  4. 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.

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

  • Related