Skip to content

Instantly share code, notes, and snippets.

@jayesh15111988
Created April 26, 2020 21:37
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jayesh15111988/b95030bca927304fc31e8cbc0123f72f to your computer and use it in GitHub Desktop.
Save jayesh15111988/b95030bca927304fc31e8cbc0123f72f to your computer and use it in GitHub Desktop.
A gist for image downloader and caching library written in Swift for iOS applications
import UIKit
// Image downloader utility class. We are going to use the singleton instance to be able to download required images and store them into in-memory cache.
final class ImageDownloader {
static let shared = ImageDownloader()
private var cachedImages: [String: UIImage]
private var imagesDownloadTasks: [String: URLSessionDataTask]
// A serial queue to be able to write the non-thread-safe dictionary
let serialQueueForImages = DispatchQueue(label: "images.queue", attributes: .concurrent)
let serialQueueForDataTasks = DispatchQueue(label: "dataTasks.queue", attributes: .concurrent)
// MARK: Private init
private init() {
cachedImages = [:]
imagesDownloadTasks = [:]
}
/**
Downloads and returns images through the completion closure to the caller
- Parameter imageUrlString: The remote URL to download images from
- Parameter completionHandler: A completion handler which returns two parameters. First one is an image which may or may
not be cached and second one is a bool to indicate whether we returned the cached version or not
- Parameter placeholderImage: Placeholder image to display as we're downloading them from the server
*/
func downloadImage(with imageUrlString: String?,
completionHandler: @escaping (UIImage?, Bool) -> Void,
placeholderImage: UIImage?) {
guard let imageUrlString = imageUrlString else {
completionHandler(placeholderImage, true)
return
}
if let image = getCachedImageFrom(urlString: imageUrlString) {
completionHandler(image, true)
} else {
guard let url = URL(string: imageUrlString) else {
completionHandler(placeholderImage, true)
return
}
if let _ = getDataTaskFrom(urlString: imageUrlString) {
return
}
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else {
return
}
if let _ = error {
DispatchQueue.main.async {
completionHandler(placeholderImage, true)
}
return
}
let image = UIImage(data: data)
self.serialQueueForImages.sync(flags: .barrier) {
self.cachedImages[imageUrlString] = image
}
_ = self.serialQueueForDataTasks.sync(flags: .barrier) {
self.imagesDownloadTasks.removeValue(forKey: imageUrlString)
}
DispatchQueue.main.async {
completionHandler(image, false)
}
}
// We want to control the access to no-thread-safe dictionary in case it's being accessed by multiple threads at once
self.serialQueueForDataTasks.sync(flags: .barrier) {
imagesDownloadTasks[imageUrlString] = task
}
task.resume()
}
}
private func cancelPreviousTask(with urlString: String?) {
if let urlString = urlString, let task = getDataTaskFrom(urlString: urlString) {
task.cancel()
// Since Swift dictionaries are not thread-safe, we have to explicitly set this barrier to avoid fatal error when it is accessed by multiple threads simultaneously
_ = serialQueueForDataTasks.sync(flags: .barrier) {
imagesDownloadTasks.removeValue(forKey: urlString)
}
}
}
private func getCachedImageFrom(urlString: String) -> UIImage? {
// Reading from the dictionary should happen in the thread-safe manner.
serialQueueForImages.sync {
return cachedImages[urlString]
}
}
private func getDataTaskFrom(urlString: String) -> URLSessionTask? {
// Reading from the dictionary should happen in the thread-safe manner.
serialQueueForDataTasks.sync {
return imagesDownloadTasks[urlString]
}
}
}
@Gonzalo-MR8
Copy link

This is a good solution to leave to the user the minimum wait time and reduce the developer work to do it, it helps my a lot thanks!

@jayesh15111988
Copy link
Author

Thank you @GonzaMaReg 🎉

@paulsoham
Copy link

Very good !

@TeaDoan
Copy link

TeaDoan commented Mar 2, 2023

@jayesh15111988 Great work!
Just curious shouldn't we remove the task if there is an error before return? Or it's intentional? Thanks

@jayesh15111988
Copy link
Author

@jayesh15111988 Great work! Just curious shouldn't we remove the task if there is an error before return? Or it's intentional? Thanks

Do you mean if we return early even before creating a task instance? If that's your question, we don't need to cancel the task as it may already be downloading previous images. If I misunderstood, could you be more elaborate?

@TeaDoan
Copy link

TeaDoan commented Mar 2, 2023

@jayesh15111988 In the completion handler for the task, on line 57 we check if there's an error. If so, we call the completion handler and return, but because we return early, we do not remove the task from the tasks dictionary as we normally would on line 70. Could you explain why? Thank you.

@jayesh15111988
Copy link
Author

@jayesh15111988 In the completion handler for the task, on line 57 we check if there's an error. If so, we call the completion handler and return, but because we return early, we do not remove the task from the tasks dictionary as we normally would on line 70. Could you explain why? Thank you.

Thank you for pointing it out. Yes, you're right. We should be removing relevant task from dictionary. I will make a fix over this weekend. Appreciate the extra eye here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment