diff --git a/ImageViewer/ViewController.swift b/ImageViewer/ViewController.swift index 02279e8..0b90922 100644 --- a/ImageViewer/ViewController.swift +++ b/ImageViewer/ViewController.swift @@ -193,8 +193,7 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate { metadataList[dir] = metadata loadLocalState() let volCovP = ProgressIndices(v: progress.v, c: 0, i: 0) - let volumeDir = currentPath.appendingPathComponent("volumes") - let archivePath = volumeDir.appendingPathComponent(metadata.volumes[volCovP.v].archive) + let archivePath = getArchiveURL(volCovP.v) let archive = try! Archive(url: archivePath, accessMode: .read) let filename = metadata.volumes[volCovP.v].chapters[volCovP.c].images[volCovP.i].filename! let targetEntry = archive[filename]! @@ -697,67 +696,31 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate { } func readComic(name: String) { + imageView.image = nil readerView.isHidden = false homeView.isHidden = true setNeedsStatusBarAppearanceUpdate() if let path = getPathFromComicName(name: name) { currentPath = path - metadata = metadataList[path] globalState.comicName = metadata.title saveGlobalState() loadLocalState() - if mode != .scroll { - scrollingCollectionView.isHidden = true - leftView.isHidden = false - rightView.isHidden = false - setImages(path: path) + scrollingCollectionView.isHidden = mode != .scroll + leftView.isHidden = mode == .scroll + rightView.isHidden = mode == .scroll + imageView.isHidden = mode == .scroll + if mode == .scroll { + scrollingCollectionView.reloadData() } else { - setupScrolling() + setImages() } } else { print("path not found") } } - func setupScrolling() { - leftView.isHidden = true - rightView.isHidden = true - scrollingCollectionView.isHidden = false - scrollingCollectionView.reloadData() - } - - func countFiles() -> Int { - var count = 0 - if let enumerator = fileManager.enumerator( - at: currentPath, includingPropertiesForKeys: [.isDirectoryKey], - options: [.skipsHiddenFiles] - ) { - for case let dir as URL in enumerator { - do { - if let enumerator = fileManager.enumerator( - at: dir, includingPropertiesForKeys: [.isRegularFileKey], - options: [.skipsHiddenFiles] - ) { - for case let file as URL in enumerator { - let resourceValues = try file.resourceValues(forKeys: [ - .isRegularFileKey, - ]) - if resourceValues.isRegularFile == true { - count += 1 - } - } - } - - } catch { - print("Error reading file attributes for \(dir):", error) - } - } - } - return count - } - func setupScrollingCollectionView() { let layout = UICollectionViewFlowLayout() layout.minimumInteritemSpacing = 0 @@ -1044,13 +1007,11 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate { default: break } if prev == mode { return } - if mode == .scroll { - setupScrolling() - } else { - let archiveURL = currentPath.appendingPathComponent("volumes").appendingPathComponent(metadata.volumes[progress.v].archive) + if mode != .scroll { + let archiveURL = getArchiveURL(progress.v) imageLoader.loadImage( archiveURL: archiveURL, - filename: getImagePath(progress: progress), + filename: getImagePath(progress), scaling: .scaleAspectFit, screenSize: UIScreen.main.bounds.size ) { [weak self] image in @@ -1062,6 +1023,10 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate { leftView.isHidden = mode == .scroll rightView.isHidden = mode == .scroll imageView.isHidden = mode == .scroll + // This does not optmize for the case where you switch between scrolling and non-scrolling in the same comic. However this badly supported because of the lac of state conversion. + if mode == .scroll { + scrollingCollectionView.reloadData() + } togglePageTurnDropdown() saveLocalState() } @@ -1190,21 +1155,27 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate { } progress = newProgress updateInfo() - let path = getImagePath(progress: progress) + let path = getImagePath(progress) imageLoader.setActiveWindow([path]) - let archiveURL = currentPath.appendingPathComponent("volumes").appendingPathComponent(metadata.volumes[progress.v].archive) - imageLoader.loadImage(archiveURL: archiveURL, filename: getImagePath(progress: progress), scaling: scaling, screenSize: UIScreen.main.bounds.size) { [weak self] image in + imageLoader.loadImage( + archiveURL: getArchiveURL(progress.v), + filename: getImagePath(progress), + scaling: scaling, + screenSize: UIScreen.main.bounds.size + ) { [weak self] image in self?.imageView.image = image } for _ in 0 ..< preloadCount { newProgress = getProgressIndicesFromTurn(turn: .next, progress: newProgress) - let archive = metadata.volumes[newProgress.v].archive! - let filename = getImagePath(progress: newProgress) - let archiveURL = currentPath.appendingPathComponent("volumes").appendingPathComponent(archive) - imageLoader.preloadImage(archiveURL: archiveURL, filename: filename, scaling: scaling, screenSize: UIScreen.main.bounds.size) + imageLoader.preloadImage( + archiveURL: getArchiveURL(newProgress.v), + filename: getImagePath(newProgress), + scaling: scaling, + screenSize: UIScreen.main.bounds.size + ) } saveLocalState() @@ -1273,7 +1244,11 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate { info.text = text } - func getImagePath(progress: ProgressIndices) -> String { + func getArchiveURL(_ volumeProgress: Int) -> URL { + return currentPath.appendingPathComponent("volumes").appendingPathComponent(metadata.volumes[volumeProgress].archive) + } + + func getImagePath(_ progress: ProgressIndices) -> String { let (v, c, i) = (progress.v, progress.c, progress.i) let modernPath = metadata.volumes[v].chapters[c].images[i].filename! // if fileManager.fileExists(atPath: modernPath.path) { @@ -1323,11 +1298,11 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate { return metadata } - func setImages(path _: URL) { + func setImages() { let scaling: UIView.ContentMode = mode == .scroll ? .scaleAspectFill : .scaleAspectFit imageLoader.loadImage( - archiveURL: currentPath.appendingPathComponent("volumes").appendingPathComponent(metadata.volumes[progress.v].archive), - filename: getImagePath(progress: progress), + archiveURL: getArchiveURL(progress.v), + filename: getImagePath(progress), scaling: scaling, screenSize: UIScreen.main.bounds.size ) { [weak self] image in @@ -1338,7 +1313,12 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate { var newProgress = progress for _ in 0 ..< preloadCount { newProgress = getProgressIndicesFromTurn(turn: .next, progress: newProgress) - imageLoader.preloadImage(archiveURL: currentPath.appendingPathComponent("volumes").appendingPathComponent(metadata.volumes[progress.v].archive), filename: getImagePath(progress: newProgress), scaling: scaling, screenSize: UIScreen.main.bounds.size) + imageLoader.preloadImage( + archiveURL: getArchiveURL(newProgress.v), + filename: getImagePath(newProgress), + scaling: scaling, + screenSize: UIScreen.main.bounds.size, + ) } } @@ -1357,15 +1337,14 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate { super.viewWillTransition(to: size, with: coordinator) if mode != .scroll { imageLoader.loadImage( - archiveURL: currentPath.appendingPathComponent("volumes").appendingPathComponent(metadata.volumes[progress.v].archive), - filename: getImagePath(progress: progress), + archiveURL: getArchiveURL(progress.v), + filename: getImagePath(progress), scaling: .scaleAspectFit, screenSize: size ) { image in self.imageView.image = image } } else { - scrollingCollectionView.reloadData() changedOrientation = true } } @@ -1442,6 +1421,7 @@ extension ViewController: UICollectionViewDataSource, UICollectionViewDelegateFl withReuseIdentifier: "ScrollingImageCell", for: indexPath ) as! ScrollingImageCell + if mode != .scroll { return cell } if metadata == nil { print("metadata is nil, should probably not be the case") return cell @@ -1453,9 +1433,10 @@ extension ViewController: UICollectionViewDataSource, UICollectionViewDelegateFl } } imageLoader.loadImage( - archiveURL: currentPath.appendingPathComponent("volumes").appendingPathComponent(metadata.volumes[progress.v].archive), - filename: getImagePath(progress: newProgress), - scaling: scaling, screenSize: UIScreen.main.bounds.size) { image in + archiveURL: getArchiveURL(progress.v), + filename: getImagePath(newProgress), + scaling: scaling, screenSize: UIScreen.main.bounds.size + ) { image in cell.imageView.image = image } return cell @@ -1568,6 +1549,7 @@ enum Rotation { } class ImageLoader { + private let cache = NSCache() private let portraitFitCache = NSCache() private let landscapeFitCache = NSCache() private let portraitFillCache = NSCache() @@ -1578,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 @@ -1613,21 +1596,14 @@ class ImageLoader { if rotation == .horizontal, let cached = landscapeFillCache.object(forKey: key as NSString) { completion?(cached); return } - } else { - print("this should not be reachable") - return - } - - if let existing = activeTokens[key], !existing.isCancelled { - if let completion = completion { - loadingTasks[key] = completion - } - return - } + } else { assertionFailure() } if let completion = completion { loadingTasks[key] = completion } + if let existing = activeTokens[key], !existing.isCancelled { + return + } let token = CancellationToken() activeTokens[key] = token @@ -1635,84 +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 var image = UIImage(data: extractedData) else { - finish(nil) + guard let image = cache.object(forKey: key as NSString) ?? UIImage(data: extractData()) else { + finish() return } - if token.isCancelled { finish(nil); return } + self.cache.setObject(image, forKey: filename as NSString, cost: imageByteSize(image)!) - // 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 { image = vScaleImage } - } + if token.isCancelled { finish(); return } - if token.isCancelled { finish(nil); return } + let scaledImage = scaleAndCacheImage( + image: image, + orientation: rotation, + screenSize: screenSize, + scaling: scaling, + filename: key, + ) - 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 { image = hScaleImage } - } + if token.isCancelled { finish(); return } - finish(image) + setImage(scaledImage) + + 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) {