cache image dimensions and (chapter, page) used for scrolling, fix bug related to width and height of UICollectionViewCell

This commit is contained in:
2025-07-26 00:32:49 +02:00
parent 2f6f29603a
commit 6a9dbe2f0b
2 changed files with 150 additions and 51 deletions

View File

@@ -3,11 +3,15 @@
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>uploadBitcode</key>
<false/>
<key>uploadSymbols</key>
<key>method</key>
<string>debugging</string>
<key>signingStyle</key>
<string>automatic</string>
<key>destination</key>
<string>export</string>
<key>stripSwiftSymbols</key>
<true/>
<key>compileBitcode</key>
<true/>
<false/>
</dict>
</plist>

View File

@@ -1,11 +1,13 @@
//TODO: Implement support for bonus chapters
//TODO: Implement Anilist support?
//TODO: Properly avoid swallowing of input from UICollectionView used for scrolling
//TODO: Fix UICollectionView sizeForItemAt method performance either by caching or lazy loading
//TODO: Convert between state for normal and scrolling page turn
import UIKit
let preloadCount = 3
let preloadCount = 2
// With a whopping 2 cores, and 2 threads because no hyperthreading, it makes sense to only have a queue as an extra thread to do work with to allow the main thread for ui.
let queue = DispatchQueue(label: "queue", qos: .utility)
enum PageTurn {
case next
@@ -69,6 +71,9 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
var scrollPos: CGPoint!
var hasSetContentOffset = false
var pageCount = 0
var pagesAvailable = 0
var sizeList: [CGSize] = []
var chapterAndPages: [(Int, Int)] = []
var imageView = UIImageView()
var mode = PageTurnMode.leftToRight
@@ -106,12 +111,10 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
let imageLoader = ImageLoader.init()
let localStateQueue = DispatchQueue(label: "state.local.queue", qos: .background)
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .clear
view.backgroundColor = .white
setup()
}
@@ -249,10 +252,10 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
case .leftToRight:
ReadProgress.leftToRight(chapter: self.currentChapter, page: self.currentPage)
case .rightToLeft:
ReadProgress.leftToRight(chapter: self.currentChapter, page: self.currentPage)
ReadProgress.rightToLeft(chapter: self.currentChapter, page: self.currentPage)
case .scroll: ReadProgress.scroll(scrollingCollectionView.contentOffset)
}
localStateQueue.async {
queue.async {
do {
try JSONEncoder().encode(
LocalState(
@@ -285,6 +288,17 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
case .scroll(let point):
if self.scrollPos == nil {
self.scrollPos = point
let centerPoint = CGPoint(
x: scrollingCollectionView.bounds.midX
+ scrollingCollectionView.contentOffset.x,
y: scrollingCollectionView.bounds.midY
+ scrollingCollectionView.contentOffset.y)
if let indexPath = scrollingCollectionView.indexPathForItem(at: centerPoint) {
let (chapter, page) = chapterAndPages[indexPath.item]
currentChapter = chapter
currentPage = page
}
}
self.mode = .scroll
}
@@ -313,13 +327,7 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
y: scrollingCollectionView.bounds.midY + scrollingCollectionView.contentOffset.y)
if let indexPath = scrollingCollectionView.indexPathForItem(at: centerPoint) {
var index = 0
var (chapter, page) = (1, 1)
while index < indexPath.item {
(chapter, page) = getChapterAndPageFromTurn(
chapter: chapter, page: page, turn: .next)
index += 1
}
let (chapter, page) = chapterAndPages[indexPath.item]
currentChapter = chapter
currentPage = page
}
@@ -330,7 +338,9 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if scrollingCollectionView.isHidden == false && mode == .scroll && !hasSetContentOffset {
if scrollingCollectionView.isHidden == false && mode == .scroll
&& !hasSetContentOffset
{
scrollingCollectionView.setContentOffset(scrollPos, animated: false)
hasSetContentOffset = true
}
@@ -343,7 +353,7 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
if let path = getPathFromComicName(name: name) {
currentPath = path
metadata = getMetadata(path: path)!
pageCount = metadata.chapters.map { $0.pages }.reduce(0, +)
globalState.comicName = metadata.title
saveGlobalState()
loadLocalState()
@@ -353,14 +363,80 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
rightView.isHidden = false
setImages(path: path, metadata: metadata)
} else {
leftView.isHidden = true
rightView.isHidden = true
scrollingCollectionView.isHidden = false
scrollingCollectionView.reloadData()
setupScrolling()
}
}
}
func setupScrolling() {
leftView.isHidden = true
rightView.isHidden = true
scrollingCollectionView.isHidden = false
pagesAvailable = countFiles()
var index = 0
var (chapter, page) = (1, 1)
chapterAndPages = Array(repeating: (0, 0), count: pagesAvailable + 1)
chapterAndPages[index] = (chapter, page)
while index < pagesAvailable {
index += 1
(chapter, page) = getChapterAndPageFromTurn(
chapter: chapter, page: page, turn: .next)
chapterAndPages[index] = (chapter, page)
}
sizeList = Array(repeating: CGSize(), count: pagesAvailable)
let binPath = currentPath.appendingPathComponent("size.bin")
let decoder = PropertyListDecoder()
do {
let data = try Data(contentsOf: binPath)
sizeList = try decoder.decode([CGSize].self, from: data)
} catch {
print("Failed to read data:", error)
}
scrollingCollectionView.reloadData()
DispatchQueue.main.async {
let encoder = PropertyListEncoder()
encoder.outputFormat = .binary
do {
let data = try encoder.encode(self.sizeList)
try data.write(to: binPath)
} catch {
print("Encoding or writing failed:", error)
}
}
}
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
@@ -368,7 +444,6 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
scrollingCollectionView = UICollectionView(
frame: view.bounds, collectionViewLayout: layout)
scrollingCollectionView.translatesAutoresizingMaskIntoConstraints = false
scrollingCollectionView.isUserInteractionEnabled = true
scrollingCollectionView.dataSource = self
scrollingCollectionView.delegate = self
scrollingCollectionView.backgroundColor = .white
@@ -383,7 +458,6 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
scrollingCollectionView.trailingAnchor.constraint(equalTo: readerView.trailingAnchor),
scrollingCollectionView.heightAnchor.constraint(equalTo: readerView.heightAnchor),
])
}
func setupTapZones() {
@@ -639,16 +713,28 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
@objc func handlePageTurnOption(_ sender: UIButton) {
if let title = sender.currentTitle {
let prev = mode
switch title.lowercased() {
case "left to right": mode = .leftToRight
case "right to left": mode = .rightToLeft
case "scroll":
if mode != .scroll {
scrollingCollectionView.reloadData()
}
mode = .scroll
case "scroll": mode = .scroll
default: break
}
if prev == mode { return }
if mode == .scroll {
setupScrolling()
} else {
imageLoader.loadImage(
at: getImagePath(
chapter: currentChapter,
volume: Int(metadata.chapters[currentChapter - 1].volume),
page: currentPage,
path: currentPath)
) {
[weak self] image in
self?.imageView.image = image
}
}
}
scrollingCollectionView.isHidden = mode != .scroll
leftView.isHidden = mode == .scroll
@@ -824,7 +910,7 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
if let chapterTitle = metadata.chapters[currentChapter - 1].title {
text =
text + "\(chapterTitle)"
text + "\(chapterTitle)\n"
}
text =
text + """
@@ -1002,7 +1088,7 @@ extension ViewController: UICollectionViewDataSource, UICollectionViewDelegateFl
if collectionView == comicCollectionView {
return comics.count
} else {
return pageCount
return pagesAvailable
}
}
@@ -1024,20 +1110,26 @@ extension ViewController: UICollectionViewDataSource, UICollectionViewDelegateFl
if metadata == nil {
return cell
}
var index = 0
var (chapter, page) = (1, 1)
while index < indexPath.item {
(chapter, page) = getChapterAndPageFromTurn(
chapter: chapter, page: page, turn: .next)
index += 1
}
var (chapter, page) = chapterAndPages[indexPath.item]
if let url = getImagePath(
chapter: chapter,
volume: Int(metadata.chapters[chapter - 1].volume),
page: page, path: currentPath)
{
// cell.imageView.image = UIImage(contentsOfFile: url.path)
imageLoader.loadImage(at: url) { image in cell.imageView.image = image }
for _ in 0...preloadCount {
(chapter, page) = getChapterAndPageFromTurn(
chapter: chapter, page: page, turn: .next)
if let url = getImagePath(
chapter: chapter,
volume: Int(metadata.chapters[chapter - 1].volume),
page: page, path: currentPath)
{
imageLoader.preloadImage(at: url)
} else {
break
}
}
}
return cell
} else {
@@ -1066,13 +1158,13 @@ extension ViewController: UICollectionViewDataSource, UICollectionViewDelegateFl
if metadata == nil {
return CGSize()
}
var index = 0
var (chapter, page) = (1, 1)
while index < indexPath.item {
(chapter, page) = getChapterAndPageFromTurn(
chapter: chapter, page: page, turn: .next)
index += 1
if sizeList[indexPath.item] != CGSize(width: 0, height: 0) {
return CGSize(
width: readerView.bounds.width,
height: sizeList[indexPath.item].height * readerView.bounds.width
/ sizeList[indexPath.item].width)
}
let (chapter, page) = chapterAndPages[indexPath.item]
if let imagePath = getImagePath(
chapter: chapter,
volume: Int(metadata.chapters[chapter - 1].volume),
@@ -1080,11 +1172,15 @@ extension ViewController: UICollectionViewDataSource, UICollectionViewDelegateFl
let imageSource = CGImageSourceCreateWithURL(imagePath as CFURL, nil),
let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil)
as? [CFString: Any],
let height = imageProperties[kCGImagePropertyPixelHeight] as? CGFloat
let height = imageProperties[kCGImagePropertyPixelHeight] as? CGFloat,
let width = imageProperties[kCGImagePropertyPixelWidth] as? CGFloat
{
return CGSize(width: readerView.bounds.width, height: height)
sizeList[indexPath.item] = CGSize(width: width, height: height)
return CGSize(
width: readerView.bounds.width, height: height * readerView.bounds.width / width
)
}
return CGSize(width: readerView.bounds.width, height: readerView.bounds.height)
return CGSize()
} else {
assert(false)
}
@@ -1152,7 +1248,6 @@ class ComicImageCell: UICollectionViewCell {
class ImageLoader {
private let cache = NSCache<NSString, UIImage>()
private var loadingTasks: [String: [((UIImage?) -> Void)]] = [:]
private let queue = DispatchQueue(label: "image.loading.queue", qos: .userInitiated)
init() {
cache.totalCostLimit = 50 * 1024 * 1024