Created
March 27, 2024 06:26
-
-
Save hishd/824455d2d424c1b358a12e1b24f6a654 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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() | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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