150 lines
5.1 KiB
Swift
150 lines
5.1 KiB
Swift
//
|
|
// 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<AnyObject, AnyObject> = {
|
|
let cache = NSCache<AnyObject, AnyObject>()
|
|
cache.countLimit = config.countLimit
|
|
return cache
|
|
}()
|
|
// 2nd level cache, that contains decoded images
|
|
private lazy var decodedImageCache: NSCache<AnyObject, AnyObject> = {
|
|
let cache = NSCache<AnyObject, AnyObject>()
|
|
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<UIImage?, Never> {
|
|
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()
|
|
}
|
|
}
|