diff --git a/ImageViewer/ViewController.swift b/ImageViewer/ViewController.swift index 8c5a679..69104b0 100644 --- a/ImageViewer/ViewController.swift +++ b/ImageViewer/ViewController.swift @@ -1,6 +1,5 @@ // TODO: Anilist support? // TODO: Properly avoid swallowing of input from UICollectionView used for scrolling -// TODO: Convert between state for normal and scrolling page turn // TODO: Support reading with scrolling and landscape mode import Foundation @@ -1156,16 +1155,23 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate { return } progress = newProgress + updateInfo() + var preloadPaths: [URL] = [] let path = getImagePath(progress: progress) - imageLoader.loadImage(at: path, scaling: scaling, screenSize: UIScreen.main.bounds.size) { [weak self] image in - self?.imageView.image = image - self?.updateInfo() - } for _ in 0 ..< preloadCount { newProgress = getProgressIndicesFromTurn(turn: .next, progress: newProgress) - let path = getImagePath(progress: newProgress) - imageLoader.preloadImage(at: path, scaling: scaling, screenSize: UIScreen.main.bounds.size) + preloadPaths.append(getImagePath(progress: newProgress)) + } + + imageLoader.setActiveWindow([path]) + + imageLoader.loadImage(at: path, scaling: scaling, screenSize: UIScreen.main.bounds.size) { [weak self] image in + self?.imageView.image = image + } + + for path in preloadPaths { + imageLoader.preloadImage(at: path, scaling: scaling, screenSize: UIScreen.main.bounds.size) } saveLocalState() @@ -1527,7 +1533,8 @@ enum Rotation { class ImageLoader { private let cache = NSCache() private let horCache = NSCache() - private var loadingTasks: [String: [(UIImage?) -> Void]] = [:] + private var loadingTasks: [String: (UIImage?) -> Void] = [:] + private var activeTokens: [String: CancellationToken] = [:] init() { // 128 MiB @@ -1543,35 +1550,53 @@ class ImageLoader { screenSize: CGSize, completion: ((UIImage?) -> Void)? ) { - let rotation: Rotation! - if screenSize.width > screenSize.height { - rotation = Rotation.horizontal - } else { rotation = Rotation.vertical } + let rotation: Rotation = screenSize.width > screenSize.height ? .horizontal : .vertical + let key = path.path - if rotation == .vertical { - if let cached = cache.object(forKey: path.path as NSString) { - completion?(cached) - return - } - } else { - if let cached = horCache.object(forKey: path.path as NSString) { - completion?(cached) - return - } + if rotation == .vertical, let cached = cache.object(forKey: key as NSString) { + completion?(cached); return + } + if rotation == .horizontal, let cached = horCache.object(forKey: key as NSString) { + completion?(cached); return } - if loadingTasks[path.path] != nil { + if let existing = activeTokens[key], !existing.isCancelled { if let completion = completion { - loadingTasks[path.path]?.append(completion) + loadingTasks[key] = completion } return } - loadingTasks[path.path] = completion != nil ? [completion!] : [] + if let completion = completion { + loadingTasks[key] = completion + } + + let token = CancellationToken() + activeTokens[key] = token queue.async { [weak self] in guard let self = self else { return } - guard var image = UIImage(contentsOfFile: path.path) else { return } + + func finish(_ image: UIImage?) { + 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) + } + } + } + } + + if token.isCancelled { finish(nil); return } + + guard var image = UIImage(contentsOfFile: key) else { + finish(nil) + return + } + + if token.isCancelled { finish(nil); 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 { @@ -1579,37 +1604,38 @@ class ImageLoader { width: min(screenSize.width, screenSize.height), height: max(screenSize.height, screenSize.width) ) - let vScaledSize = aspectSize( - for: image.size, in: vertical, scaling: scaling - ) + let vScaledSize = aspectSize(for: image.size, in: vertical, scaling: scaling) let vScaleImage = resizeImage(image, to: vScaledSize) - guard let cost = imageByteSize(vScaleImage) else { return } - self.cache.setObject(vScaleImage, forKey: path.path as NSString, cost: cost) - if rotation == .vertical { - image = vScaleImage + if let cost = imageByteSize(vScaleImage) { + self.cache.setObject(vScaleImage, forKey: path.path as NSString, cost: cost) } + if rotation == .vertical { image = vScaleImage } } + if token.isCancelled { finish(nil); 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 hScaledSize = aspectSize(for: image.size, in: horizontal, scaling: scaling) let hScaleImage = resizeImage(image, to: hScaledSize) - guard let cost = imageByteSize(hScaleImage) else { return } - self.horCache.setObject(hScaleImage, forKey: path.path as NSString, cost: cost) - if rotation == .horizontal { - image = hScaleImage + if let cost = imageByteSize(hScaleImage) { + self.horCache.setObject(hScaleImage, forKey: path.path as NSString, cost: cost) } + if rotation == .horizontal { image = hScaleImage } } - DispatchQueue.main.async { - self.loadingTasks[path.path]?.forEach { $0(image) } - self.loadingTasks.removeValue(forKey: path.path) - } + finish(image) + } + } + + func setActiveWindow(_ paths: [URL]) { + let desired = Set(paths.map { $0.path }) + for (key, token) in activeTokens where !desired.contains(key) { + token.cancel() + loadingTasks[key] = nil } } @@ -1648,3 +1674,18 @@ func aspectSize(for imageSize: CGSize, in boundingSize: CGSize, scaling: UIView. height: imageSize.height * scale ) } + +final class CancellationToken { + private var _isCancelled = false + private let lock = NSLock() + + var isCancelled: Bool { + lock.lock(); defer { lock.unlock() } + return _isCancelled + } + + func cancel() { + lock.lock(); defer { lock.unlock() } + _isCancelled = true + } +}