diff --git a/ImageViewer/ViewController.swift b/ImageViewer/ViewController.swift index 0c4ddf3..0b90922 100644 --- a/ImageViewer/ViewController.swift +++ b/ImageViewer/ViewController.swift @@ -1549,6 +1549,7 @@ enum Rotation { } class ImageLoader { + private let cache = NSCache() private let portraitFitCache = NSCache() private let landscapeFitCache = NSCache() private let portraitFillCache = NSCache() @@ -1559,6 +1560,7 @@ class ImageLoader { init() { // 128 MiB + cache.totalCostLimit = 128 * 1024 * 1024 portraitFitCache.totalCostLimit = 128 * 1024 * 1024 landscapeFitCache.totalCostLimit = 128 * 1024 * 1024 portraitFillCache.totalCostLimit = 128 * 1024 * 1024 @@ -1594,13 +1596,9 @@ class ImageLoader { if rotation == .horizontal, let cached = landscapeFillCache.object(forKey: key as NSString) { completion?(cached); return } - } else { - print("this should not be reachable") - return - } + } else { assertionFailure() } if let completion = completion { - assert(loadingTasks[key] == nil) loadingTasks[key] = completion } if let existing = activeTokens[key], !existing.isCancelled { @@ -1613,86 +1611,102 @@ class ImageLoader { queue.async { [weak self] in guard let self = self else { return } - func finish(_ image: UIImage?) { + func finish() { DispatchQueue.main.async { guard self.activeTokens[key] === token else { return } self.activeTokens.removeValue(forKey: key) - if let image = image { - if let callback = self.loadingTasks.removeValue(forKey: key) { - callback(image) - } + self.loadingTasks.removeValue(forKey: key) + } + } + func setImage(_ image: UIImage) { + DispatchQueue.main.async { + if let callback = self.loadingTasks[key] { + callback(image) } } } - if token.isCancelled { finish(nil); return } + if token.isCancelled { finish(); return } - var extractedData = Data() - if self.archives.object(forKey: archiveURL.lastPathComponent as NSString) == nil { - let archive = try! Archive(url: archiveURL, accessMode: .read) - self.archives.setObject(archive, forKey: archiveURL.lastPathComponent as NSString) - } - let archive = self.archives.object(forKey: archiveURL.lastPathComponent as NSString)! - _ = try! archive.extract(archive[filename]!) { data in - extractedData.append(data) + func extractData() -> Data { + var extractedData = Data() + if self.archives.object(forKey: archiveURL.lastPathComponent as NSString) == nil { + let archive = try! Archive(url: archiveURL, accessMode: .read) + self.archives.setObject(archive, forKey: archiveURL.lastPathComponent as NSString) + } + let archive = self.archives.object(forKey: archiveURL.lastPathComponent as NSString)! + _ = try! archive.extract(archive[filename]!) { data in + extractedData.append(data) + } + return extractedData } - if token.isCancelled { finish(nil); return } + if token.isCancelled { finish(); return } - guard let image = UIImage(data: extractedData) else { - finish(nil) + guard let image = cache.object(forKey: key as NSString) ?? UIImage(data: extractData()) else { + finish() return } - var finalImage: UIImage! + self.cache.setObject(image, forKey: filename as NSString, cost: imageByteSize(image)!) - if token.isCancelled { finish(nil); return } + if token.isCancelled { finish(); return } - // If you turn pages fast, completion will not be nil and as such only the needed scaled image should be prepared - if completion == nil || rotation == .vertical { - let vertical = CGSize( - width: min(screenSize.width, screenSize.height), - height: max(screenSize.height, screenSize.width) - ) - let vScaledSize = aspectSize(for: image.size, in: vertical, scaling: scaling) - let vScaleImage = resizeImage(image, to: vScaledSize) - let cost = imageByteSize(vScaleImage)! - if scaling == .scaleAspectFit { - self.portraitFitCache.setObject(vScaleImage, forKey: filename as NSString, cost: cost) - } else if scaling == .scaleAspectFill { - self.portraitFillCache.setObject(vScaleImage, forKey: filename as NSString, cost: cost) - } else { - print("this should be unreachable") - return - } - if rotation == .vertical { finalImage = vScaleImage } - } + let scaledImage = scaleAndCacheImage( + image: image, + orientation: rotation, + screenSize: screenSize, + scaling: scaling, + filename: key, + ) - if token.isCancelled { finish(nil); return } + if token.isCancelled { finish(); return } - if completion == nil || rotation == .horizontal { - let horizontal = CGSize( - width: max(screenSize.width, screenSize.height), - height: min(screenSize.height, screenSize.width) - ) - let hScaledSize = aspectSize(for: image.size, in: horizontal, scaling: scaling) - let hScaleImage = resizeImage(image, to: hScaledSize) - let cost = imageByteSize(hScaleImage)! - if scaling == .scaleAspectFit { - self.landscapeFitCache.setObject(hScaleImage, forKey: filename as NSString, cost: cost) - } else if scaling == .scaleAspectFill { - self.landscapeFillCache.setObject(hScaleImage, forKey: filename as NSString, cost: cost) - } else { - print("this should be unreachable") - return - } - if rotation == .horizontal { finalImage = hScaleImage } - } + setImage(scaledImage) - finish(finalImage) + let otherOrientation: Rotation = rotation == .vertical ? .horizontal : .vertical + + _ = scaleAndCacheImage( + image: image, + orientation: otherOrientation, + screenSize: screenSize, + scaling: scaling, + filename: key, + ) + finish() } } + func scaleAndCacheImage(image: UIImage, orientation: Rotation, screenSize: CGSize, scaling: UIView.ContentMode, filename: String) -> UIImage { + let resizeTo: CGSize = if orientation == .vertical { + CGSize( + width: min(screenSize.width, screenSize.height), + height: max(screenSize.height, screenSize.width) + ) + } else { + CGSize( + width: max(screenSize.width, screenSize.height), + height: min(screenSize.height, screenSize.width) + ) + } + let scaledSize = aspectSize(for: image.size, in: resizeTo, scaling: scaling) + let scaledImage = resizeImage(image, to: scaledSize) + let cost = imageByteSize(scaledImage)! + switch ((orientation, scaling)) { + case (.vertical, .scaleAspectFit): + self.portraitFitCache.setObject(scaledImage, forKey: filename as NSString, cost: cost) + case (.vertical, .scaleAspectFill): + self.portraitFillCache.setObject(scaledImage, forKey: filename as NSString, cost: cost) + case (.horizontal, .scaleAspectFit): + self.landscapeFitCache.setObject(scaledImage, forKey: filename as NSString, cost: cost) + case (.horizontal, .scaleAspectFill): + self.landscapeFillCache.setObject(scaledImage, forKey: filename as NSString, cost: cost) + default: + assertionFailure() + } + return scaledImage + } + func setActiveWindow(_ paths: [String]) { let desired = Set(paths) for (key, token) in activeTokens where !desired.contains(key) {