Skip to content

Instantly share code, notes, and snippets.

@hishd
Created March 27, 2024 06:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hishd/824455d2d424c1b358a12e1b24f6a654 to your computer and use it in GitHub Desktop.
Save hishd/824455d2d424c1b358a12e1b24f6a654 to your computer and use it in GitHub Desktop.
import Foundation
import UIKit
///Error enum for CachedImageLoader
enum CachedImageLoaderError: Error {
case errorLoading(Error)
case errorDecording
case cancelled
}
extension CachedImageLoaderError: LocalizedError {
var errorDescription: String? {
switch self {
case .errorLoading(_):
"An error occured during image loading"
case .errorDecording:
"An error occurred during image loading. No image data found."
case .cancelled:
"The operation was cancelled"
}
}
var failureReason: String? {
switch self {
case .errorLoading(let error):
"Could not load image. Error \(error.localizedDescription)"
case .errorDecording:
"Could not decode image data (no data found)."
case .cancelled:
"Operation was cancelled during image loading data task."
}
}
}
///CachedImageLoader is used to load the images from a URL and cache them in NSCache once successfully loaded
final class CachedImageLoader {
//Using NSCache to cache the images
private var cachedImages: NSCache = NSCache<NSURL, UIImage>()
//Storing the URLSession tasks which handles loading images
private var runningRequests: [UUID: URLSessionDataTask] = [:]
//Accessing through singleton
public static let publicCache = CachedImageLoader()
/// Loading an image with the provided url and cache the image once loaded.
/// If the image is previously cached and found, it will return through the completion handler.
/// - Parameters:
/// - url: The URL which the image should be loaded from
/// - completion: Callback which returns a UIImage if the operation is successful
/// - Returns: The UUID for each url request. This will be used to cancel the image load operation
func loadImage(from url: NSURL, completion: @escaping (Result<UIImage, Error>) -> Void) -> UUID? {
if let cachedImage = cachedImages.object(forKey: url) {
completion(.success(cachedImage))
return nil
}
let uuid = UUID()
let dataTask = URLSession.shared.dataTask(with: url as URL) { data, response, error in
let result: Result<UIImage, Error>
defer {
self.runningRequests.removeValue(forKey: uuid)
completion(result)
}
if let data = data, let image = UIImage(data: data) {
self.cachedImages.setObject(image, forKey: url, cost: data.count)
result = .success(image)
return
}
guard let error = error else {
//No error found, but no data is found as well
result = .failure(CachedImageLoaderError.errorDecording)
return
}
if (error as NSError).code == NSURLErrorCancelled {
result = .failure(CachedImageLoaderError.cancelled)
return
}
result = .failure(CachedImageLoaderError.errorLoading(error))
}
dataTask.resume()
runningRequests[uuid] = dataTask
return uuid
}
/// Cancel Image load operation using the UUID
/// - Parameters:
/// - id: The UUID instance which the task is associated with. This will be used to cancel the data task and remove it from the running requests
func cancelLoading(id: UUID) {
runningRequests[id]?.cancel()
runningRequests.removeValue(forKey: id)
}
}
//MARK: Sample implementation of the Image caching and loading from URL
let url = URL(string: "Some http URL")
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CELL IDENTIFIER", for: indexPath) as! SampleCollectionViewCell
cell.loadData(url: url! as NSURL)
return cell
}
class SampleCollectionViewCell: UICollectionViewCell {
lazy var imageView: UIImageView = {
let view = UIImageView()
return view
}()
func loadData(url: NSURL) {
imageView.loadImage(from: url)
}
// Reusing the cell to render another item
override func prepareForReuse() {
super.prepareForReuse()
imageView.image = nil
imageView.cancelLoading()
}
}
import UIKit
/// A helper class which is used to access the CachedImageLoader and load the image to the ImageView
final class UIImageLoader {
public static let shared = UIImageLoader()
private let cachedImageLoader = CachedImageLoader.publicCache
private var uuidDict = [UIImageView: UUID]()
private init() {}
/// Loading an image to an ImageView using the provided URL
/// - Parameters:
/// - url: The URL of the resource
/// - imageView: The ImageView instance which the image should be loaded into
/// - errorPlaceholderImage: A placeholder image which is loaded into the ImageView if the operation fails
func load(from url: NSURL, for imageView: UIImageView, errorPlaceholderImage: UIImage? = nil) {
let token = cachedImageLoader.loadImage(from: url) { result in
defer {
self.uuidDict.removeValue(forKey: imageView)
}
DispatchQueue.main.async {
switch result {
case .success(let image):
imageView.image = image
case .failure(let error):
print(error.localizedDescription)
imageView.image = errorPlaceholderImage
}
}
}
if let token = token {
self.uuidDict[imageView] = token
}
}
/// Cancelling the image loading operation if it's no longer needed (eg: preparing the cells for reusing)
/// - Parameters:
/// - imageView: The ImageView instance which the request should be cancelled with
func cancel(for imageView: UIImageView) {
if let token = self.uuidDict[imageView] {
cachedImageLoader.cancelLoading(id: token)
self.uuidDict.removeValue(forKey: imageView)
}
}
}
import UIKit
/// Extensions for the UIIMageView type to enable calling the declared methods from Self
extension UIImageView {
/// Loading the image using the provided URL through UIImageLoader
func loadImage(from url: NSURL) {
UIImageLoader.shared.load(from: url, for: self)
}
/// Cancelling the image loading through UIImageLoader
func cancelLoading() {
UIImageLoader.shared.cancel(for: self)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment