// // ImageManager.swift // VDS // // Created by Matt Bruce on 1/26/23. // import Foundation import UIKit.UIImage import Combine // Declares in-memory image cache public protocol ImageCacheType: class { // Returns the image associated with a given url func image(for url: URL) -> UIImage? // Inserts the image of the specified url in the cache func insertImage(_ image: UIImage?, for url: URL) // Removes the image of the specified url in the cache func removeImage(for url: URL) // Removes all images from the cache func removeAllImages() // Accesses the value associated with the given key for reading and writing subscript(_ url: URL) -> UIImage? { get set } } public final class ImageCache: ImageCacheType { // 1st level cache, that contains encoded images private lazy var imageCache: NSCache = { let cache = NSCache() cache.countLimit = config.countLimit return cache }() // 2nd level cache, that contains decoded images private lazy var decodedImageCache: NSCache = { let cache = NSCache() cache.totalCostLimit = config.memoryLimit return cache }() private let lock = NSLock() private let config: Config public struct Config { public let countLimit: Int public let memoryLimit: Int public static let defaultConfig = Config(countLimit: 100, memoryLimit: 1024 * 1024 * 100) // 100 MB } public init(config: Config = Config.defaultConfig) { self.config = config } public func image(for url: URL) -> UIImage? { lock.lock(); defer { lock.unlock() } // the best case scenario -> there is a decoded image in memory if let decodedImage = decodedImageCache.object(forKey: url as AnyObject) as? UIImage { return decodedImage } // search for image data if let image = imageCache.object(forKey: url as AnyObject) as? UIImage { let decodedImage = image.decodedImage() decodedImageCache.setObject(image as AnyObject, forKey: url as AnyObject, cost: decodedImage.diskSize) return decodedImage } return nil } public func insertImage(_ image: UIImage?, for url: URL) { guard let image = image else { return removeImage(for: url) } let decompressedImage = image.decodedImage() lock.lock(); defer { lock.unlock() } imageCache.setObject(decompressedImage, forKey: url as AnyObject, cost: 1) decodedImageCache.setObject(image as AnyObject, forKey: url as AnyObject, cost: decompressedImage.diskSize) } public func removeImage(for url: URL) { lock.lock(); defer { lock.unlock() } imageCache.removeObject(forKey: url as AnyObject) decodedImageCache.removeObject(forKey: url as AnyObject) } public func removeAllImages() { lock.lock(); defer { lock.unlock() } imageCache.removeAllObjects() decodedImageCache.removeAllObjects() } public subscript(_ key: URL) -> UIImage? { get { return image(for: key) } set { return insertImage(newValue, for: key) } } } fileprivate extension UIImage { func decodedImage() -> UIImage { guard let cgImage = cgImage else { return self } let size = CGSize(width: cgImage.width, height: cgImage.height) let colorSpace = CGColorSpaceCreateDeviceRGB() let context = CGContext(data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: 8, bytesPerRow: cgImage.bytesPerRow, space: colorSpace, bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue) context?.draw(cgImage, in: CGRect(origin: .zero, size: size)) guard let decodedImage = context?.makeImage() else { return self } return UIImage(cgImage: decodedImage) } // Rough estimation of how much memory image uses in bytes var diskSize: Int { guard let cgImage = cgImage else { return 0 } return cgImage.bytesPerRow * cgImage.height } } public final class ImageLoader { public static let shared = ImageLoader() private let cache: ImageCacheType private lazy var backgroundQueue: OperationQueue = { let queue = OperationQueue() queue.maxConcurrentOperationCount = 5 return queue }() public init(cache: ImageCacheType = ImageCache()) { self.cache = cache } public func loadImage(from url: URL) -> AnyPublisher { if let image = cache[url] { return Just(image).eraseToAnyPublisher() } return URLSession.shared.dataTaskPublisher(for: url) .map { (data, response) -> UIImage? in return UIImage(data: data) } .catch { error in return Just(nil) } .handleEvents(receiveOutput: {[unowned self] image in guard let image = image else { return } self.cache[url] = image }) .print("Image loading \(url):") .subscribe(on: backgroundQueue) .receive(on: RunLoop.main) .eraseToAnyPublisher() } }