diff --git a/Tusker/Caching/ImageCache.swift b/Tusker/Caching/ImageCache.swift index b65fcea5..38f79435 100644 --- a/Tusker/Caching/ImageCache.swift +++ b/Tusker/Caching/ImageCache.swift @@ -11,7 +11,7 @@ import Cache class ImageCache { - static let avatars = ImageCache(name: "Avatars", memoryExpiry: .seconds(60 * 60), diskExpiry: .seconds(60 * 60 * 24)) + static let avatars = ImageCache(name: "Avatars", memoryExpiry: .seconds(60 * 60), diskExpiry: .seconds(60 * 60 * 24), desiredSize: CGSize(width: 50, height: 50)) static let headers = ImageCache(name: "Headers", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60)) static let attachments = ImageCache(name: "Attachments", memoryExpiry: .seconds(60 * 2)) static let emojis = ImageCache(name: "Emojis", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60)) @@ -28,8 +28,10 @@ class ImageCache { private var backgroundQueue = DispatchQueue(label: "ImageCache completion queue", qos: .default) - init(name: String, memoryExpiry: Expiry, diskExpiry: Expiry? = nil) { - self.cache = ImageDataCache(name: name, memoryExpiry: memoryExpiry, diskExpiry: diskExpiry, storeOriginalDataInMemory: diskExpiry == nil) + init(name: String, memoryExpiry: Expiry, diskExpiry: Expiry? = nil, desiredSize: CGSize? = nil) { + // todo: might not always want to use UIScreen.main for this, e.g. Catalyst? + let pixelSize = desiredSize?.applying(.init(scaleX: UIScreen.main.scale, y: UIScreen.main.scale)) + self.cache = ImageDataCache(name: name, memoryExpiry: memoryExpiry, diskExpiry: diskExpiry, storeOriginalDataInMemory: diskExpiry == nil, desiredPixelSize: pixelSize) } func get(_ url: URL, completion: ((Data?, UIImage?) -> Void)?) -> Request? { @@ -49,9 +51,8 @@ class ImageCache { return group.addCallback(completion) } else { let group = RequestGroup(url: url) { (data, image) in - if let data = data, - let image = UIImage(data: data) { - try? self.cache.set(key, data: data, image: image) + if let data = data { + try? self.cache.set(key, data: data) } self.groups.removeValueWithoutReturning(forKey: url) } diff --git a/Tusker/Caching/ImageDataCache.swift b/Tusker/Caching/ImageDataCache.swift index ce895a00..a8a9e87d 100644 --- a/Tusker/Caching/ImageDataCache.swift +++ b/Tusker/Caching/ImageDataCache.swift @@ -15,8 +15,9 @@ class ImageDataCache { private let disk: DiskStorage? private let storeOriginalDataInMemory: Bool + private let desiredPixelSize: CGSize? - init(name: String, memoryExpiry: Expiry, diskExpiry: Expiry?, storeOriginalDataInMemory: Bool) { + init(name: String, memoryExpiry: Expiry, diskExpiry: Expiry?, storeOriginalDataInMemory: Bool, desiredPixelSize: CGSize?) { let memoryConfig = MemoryConfig(expiry: memoryExpiry) self.memory = MemoryStorage(config: memoryConfig) @@ -28,6 +29,7 @@ class ImageDataCache { } self.storeOriginalDataInMemory = storeOriginalDataInMemory + self.desiredPixelSize = desiredPixelSize } func has(_ key: String) throws -> Bool { @@ -42,10 +44,9 @@ class ImageDataCache { } func get(_ key: String) throws -> Entry? { - if try memory.existsObject(forKey: key) { - return try! memory.object(forKey: key) + if let memoryEntry = try? memory.object(forKey: key) { + return memoryEntry } else if let disk = self.disk, - try disk.existsObject(forKey: key), let data = try? disk.object(forKey: key), let image = UIImage(data: data) { return Entry(data: data, image: image) @@ -62,7 +63,9 @@ class ImageDataCache { return try get(key)?.data } - func set(_ key: String, data: Data, image: UIImage) throws { + func set(_ key: String, data: Data) throws { + guard let image = scaleImageIfDesired(data: data) else { return } + let entry = Entry(data: storeOriginalDataInMemory ? data : nil, image: image) memory.setObject(entry, forKey: key) @@ -76,6 +79,27 @@ class ImageDataCache { try? disk?.removeAll() } + private func scaleImageIfDesired(data: Data) -> UIImage? { + guard let desiredPixelSize = desiredPixelSize, + let source = CGImageSourceCreateWithData(data as CFData, [kCGImageSourceShouldCache: false] as CFDictionary) else { + return UIImage(data: data) + } + + let maxDimension = max(desiredPixelSize.width, desiredPixelSize.height) + let downsampleOptions = [ + kCGImageSourceCreateThumbnailFromImageAlways: true, + kCGImageSourceShouldCacheImmediately: true, + kCGImageSourceCreateThumbnailWithTransform: true, + kCGImageSourceThumbnailMaxPixelSize: maxDimension + ] as CFDictionary + + if let downsampled = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOptions) { + return UIImage(cgImage: downsampled) + } else { + return UIImage(data: data) + } + } + } extension ImageDataCache {