async I/O for images and local state
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
//TODO: Implement support for bonus chapters
|
//TODO: Implement support for bonus chapters
|
||||||
//TODO: Implement scrolling support
|
//TODO: Implement scrolling support
|
||||||
//TODO: Actually implement caching and do I/O on background thread
|
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
|
let preloadCount = 10
|
||||||
|
|
||||||
enum PageTurn {
|
enum PageTurn {
|
||||||
case next
|
case next
|
||||||
case previous
|
case previous
|
||||||
@@ -59,9 +60,6 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
|
|||||||
var readerView = UIView()
|
var readerView = UIView()
|
||||||
|
|
||||||
var imageView = UIImageView()
|
var imageView = UIImageView()
|
||||||
var previousImage: UIImage?
|
|
||||||
var currentImage: UIImage?
|
|
||||||
var nextImage: UIImage?
|
|
||||||
var mode = PageTurnMode.leftToRight
|
var mode = PageTurnMode.leftToRight
|
||||||
var metadata: Metadata!
|
var metadata: Metadata!
|
||||||
var currentPage: Int! = nil
|
var currentPage: Int! = nil
|
||||||
@@ -92,6 +90,10 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
|
|||||||
var comics: [Comic] = []
|
var comics: [Comic] = []
|
||||||
var collectionView: UICollectionView!
|
var collectionView: UICollectionView!
|
||||||
|
|
||||||
|
let imageLoader = ImageLoader.init()
|
||||||
|
|
||||||
|
let localStateQueue = DispatchQueue(label: "state.local.queue", qos: .background)
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
@@ -223,16 +225,19 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func saveLocalState() {
|
func saveLocalState() {
|
||||||
do {
|
let color = self.readerView.backgroundColor ?? .white
|
||||||
try JSONEncoder().encode(
|
localStateQueue.async {
|
||||||
LocalState(
|
do {
|
||||||
chapter: currentChapter, page: currentPage, turnMode: mode,
|
try JSONEncoder().encode(
|
||||||
backgroundColor: convertColorToString(
|
LocalState(
|
||||||
color: readerView.backgroundColor ?? .white))
|
chapter: self.currentChapter, page: self.currentPage, turnMode: self.mode,
|
||||||
).write(
|
backgroundColor: self.convertColorToString(
|
||||||
to: currentPath.appendingPathComponent("state.json"))
|
color: color))
|
||||||
} catch {
|
).write(
|
||||||
print("failed to save local state")
|
to: self.currentPath.appendingPathComponent("state.json"))
|
||||||
|
} catch {
|
||||||
|
print("failed to save local state")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,10 +280,10 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
|
|||||||
saveGlobalState()
|
saveGlobalState()
|
||||||
loadLocalState()
|
loadLocalState()
|
||||||
setImages(path: path, metadata: metadata)
|
setImages(path: path, metadata: metadata)
|
||||||
imageView.image = currentImage
|
|
||||||
info.text = """
|
info.text = """
|
||||||
|
\(metadata.title)
|
||||||
\(metadata.chapters[currentChapter - 1].title)
|
\(metadata.chapters[currentChapter - 1].title)
|
||||||
Volume \(Int(metadata.chapters[currentChapter - 1].volume))
|
Volume \(Int(metadata.chapters[currentChapter - 1].volume)) of \(Int(metadata.chapters.last!.volume))
|
||||||
Chapter \(currentChapter!) of \(Int(metadata.chapters.last!.chapter))
|
Chapter \(currentChapter!) of \(Int(metadata.chapters.last!.chapter))
|
||||||
Page \(currentPage!) of \(metadata.chapters[currentChapter - 1].pages)
|
Page \(currentPage!) of \(metadata.chapters[currentChapter - 1].pages)
|
||||||
"""
|
"""
|
||||||
@@ -363,8 +368,7 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
|
|||||||
|
|
||||||
func setupBottomBarInfo() {
|
func setupBottomBarInfo() {
|
||||||
info.textAlignment = .center
|
info.textAlignment = .center
|
||||||
info.numberOfLines = 0
|
info.numberOfLines = 5
|
||||||
info.text = ""
|
|
||||||
info.translatesAutoresizingMaskIntoConstraints = false
|
info.translatesAutoresizingMaskIntoConstraints = false
|
||||||
info.textColor = .white
|
info.textColor = .white
|
||||||
bottomBarView.addSubview(info)
|
bottomBarView.addSubview(info)
|
||||||
@@ -399,7 +403,7 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
|
|||||||
readerView.isHidden = true
|
readerView.isHidden = true
|
||||||
homeView.isHidden = false
|
homeView.isHidden = false
|
||||||
globalState.comicName = nil
|
globalState.comicName = nil
|
||||||
hideTopBar()
|
hideBar()
|
||||||
|
|
||||||
setNeedsStatusBarAppearanceUpdate()
|
setNeedsStatusBarAppearanceUpdate()
|
||||||
saveGlobalState()
|
saveGlobalState()
|
||||||
@@ -585,7 +589,7 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func toggleTopBar() {
|
func toggleBar() {
|
||||||
topBarView.isHidden.toggle()
|
topBarView.isHidden.toggle()
|
||||||
bottomBarView.isHidden.toggle()
|
bottomBarView.isHidden.toggle()
|
||||||
|
|
||||||
@@ -595,14 +599,15 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func hideTopBar() {
|
func hideBar() {
|
||||||
topBarView.isHidden = true
|
topBarView.isHidden = true
|
||||||
|
bottomBarView.isHidden = true
|
||||||
pageTurnDropdownView.isHidden = true
|
pageTurnDropdownView.isHidden = true
|
||||||
backgroundColorDropdownView.isHidden = true
|
backgroundColorDropdownView.isHidden = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func handleTopTap() {
|
@objc func handleTopTap() {
|
||||||
toggleTopBar()
|
toggleBar()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func handleLeftTap() {
|
@objc func handleLeftTap() {
|
||||||
@@ -655,52 +660,61 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func changeImage(turn: PageTurn) {
|
func changeImage(turn: PageTurn) {
|
||||||
let (chapter, page) = getChapterAndPageFromTurn(
|
var (chapter, page) = getChapterAndPageFromTurn(
|
||||||
chapter: currentChapter, page: currentPage, turn: turn)
|
chapter: currentChapter, page: currentPage, turn: turn)
|
||||||
if (chapter, page) == (currentChapter, currentPage) { return }
|
if (chapter, page) == (currentChapter, currentPage) { return }
|
||||||
let (c, p) = getChapterAndPageFromTurn(chapter: chapter, page: page, turn: turn)
|
var vol = Int(metadata.chapters[chapter - 1].volume)
|
||||||
let vol = Int(metadata.chapters[currentChapter - 1].volume)
|
if let path = getImagePath(chapter: chapter, volume: vol, page: page, path: currentPath) {
|
||||||
switch turn {
|
imageLoader.loadImage(at: path) {
|
||||||
case .next:
|
[weak self] image in
|
||||||
if nextImage == nil { return }
|
self?.imageView.image = image
|
||||||
previousImage = currentImage
|
}
|
||||||
currentImage = nextImage
|
} else {
|
||||||
nextImage = getImage(chapter: c, volume: vol, page: p, path: self.currentPath)
|
return
|
||||||
case .previous:
|
|
||||||
if previousImage == nil { return }
|
|
||||||
nextImage = currentImage
|
|
||||||
currentImage = previousImage
|
|
||||||
previousImage = getImage(chapter: c, volume: vol, page: p, path: self.currentPath)
|
|
||||||
}
|
}
|
||||||
currentPage = page
|
currentPage = page
|
||||||
currentChapter = chapter
|
currentChapter = chapter
|
||||||
imageView.image = currentImage
|
|
||||||
|
for _ in 0...preloadCount {
|
||||||
|
(chapter, page) = getChapterAndPageFromTurn(
|
||||||
|
chapter: chapter, page: page, turn: .next)
|
||||||
|
vol = Int(metadata.chapters[chapter - 1].volume)
|
||||||
|
if let path = getImagePath(
|
||||||
|
chapter: chapter, volume: vol, page: page, path: currentPath)
|
||||||
|
{
|
||||||
|
imageLoader.preloadImage(at: path)
|
||||||
|
} else {
|
||||||
|
print("could not preload image")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
saveLocalState()
|
saveLocalState()
|
||||||
info.text = """
|
info.text = """
|
||||||
|
\(metadata.title)
|
||||||
\(metadata.chapters[currentChapter - 1].title)
|
\(metadata.chapters[currentChapter - 1].title)
|
||||||
Volume \(Int(metadata.chapters[currentChapter - 1].volume))
|
Volume \(Int(metadata.chapters[currentChapter - 1].volume)) of \(Int(metadata.chapters.last!.volume))
|
||||||
Chapter \(currentChapter!) of \(Int(metadata.chapters.last!.chapter))
|
Chapter \(currentChapter!) of \(Int(metadata.chapters.last!.chapter))
|
||||||
Page \(currentPage!) of \(metadata.chapters[currentChapter - 1].pages)
|
Page \(currentPage!) of \(metadata.chapters[currentChapter - 1].pages)
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
func getImage(chapter: Int, volume: Int, page: Int, path: URL) -> UIImage! {
|
func getImagePath(chapter: Int, volume: Int, page: Int, path: URL) -> URL! {
|
||||||
let newPath = path.appendingPathComponent(String(format: "volume_%04d", volume))
|
let newPath = path.appendingPathComponent(String(format: "volume_%04d", volume))
|
||||||
.appendingPathComponent(
|
.appendingPathComponent(
|
||||||
// TODO: it should not be necessary to sub 1
|
// TODO: it should not be necessary to sub 1
|
||||||
String(format: "chapter_%04d_image_%04d.png", chapter, page - 1))
|
String(format: "chapter_%04d_image_%04d.png", chapter, page - 1))
|
||||||
if !fileManager.fileExists(atPath: newPath.path) {
|
if fileManager.fileExists(atPath: newPath.path) {
|
||||||
//TODO: Remove this
|
return newPath
|
||||||
let alternatePath = path.appendingPathComponent(String(format: "volume_%04d", volume))
|
|
||||||
.appendingPathComponent(
|
|
||||||
String(format: "chapter%03d_image_%03d.png", chapter, page - 1))
|
|
||||||
if !fileManager.fileExists(atPath: alternatePath.path) {
|
|
||||||
print("Did not find image at path")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return UIImage(contentsOfFile: alternatePath.path)!
|
|
||||||
}
|
}
|
||||||
return UIImage(contentsOfFile: newPath.path)!
|
//TODO: Remove this
|
||||||
|
let alternatePath = path.appendingPathComponent(String(format: "volume_%04d", volume))
|
||||||
|
.appendingPathComponent(
|
||||||
|
String(format: "chapter%03d_image_%03d.png", chapter, page - 1))
|
||||||
|
if !fileManager.fileExists(atPath: alternatePath.path) {
|
||||||
|
print("Did not find image at path")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return alternatePath
|
||||||
}
|
}
|
||||||
|
|
||||||
func getChapterAndPageFromTurn(chapter: Int, page: Int, turn: PageTurn) -> (Int, Int) {
|
func getChapterAndPageFromTurn(chapter: Int, page: Int, turn: PageTurn) -> (Int, Int) {
|
||||||
@@ -748,21 +762,31 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
|
|||||||
func setImages(path: URL, metadata: Metadata) {
|
func setImages(path: URL, metadata: Metadata) {
|
||||||
var directories: [URL] = []
|
var directories: [URL] = []
|
||||||
if currentPage != nil && currentChapter != nil {
|
if currentPage != nil && currentChapter != nil {
|
||||||
currentImage = getImage(
|
var vol = Int(metadata.chapters[currentChapter - 1].volume)
|
||||||
chapter: currentChapter, volume: Int(metadata.chapters[currentChapter - 1].volume),
|
imageLoader.loadImage(
|
||||||
page: currentPage, path: currentPath)
|
at: getImagePath(
|
||||||
|
chapter: currentChapter, volume: vol, page: currentPage, path: currentPath
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
[weak self] image in
|
||||||
|
self?.imageView.image = image
|
||||||
|
}
|
||||||
|
|
||||||
let (nc, np) = getChapterAndPageFromTurn(
|
var (chapter, page) = getChapterAndPageFromTurn(
|
||||||
chapter: currentChapter, page: currentPage, turn: .next)
|
chapter: currentChapter, page: currentPage, turn: .next)
|
||||||
nextImage = getImage(
|
for _ in 0...preloadCount {
|
||||||
chapter: nc, volume: Int(metadata.chapters[nc - 1].volume),
|
vol = Int(metadata.chapters[chapter - 1].volume)
|
||||||
page: np, path: currentPath)
|
if let path = getImagePath(
|
||||||
|
chapter: chapter, volume: vol, page: page, path: currentPath)
|
||||||
|
{
|
||||||
|
imageLoader.preloadImage(at: path)
|
||||||
|
} else {
|
||||||
|
print("could not preload image")
|
||||||
|
}
|
||||||
|
(chapter, page) = getChapterAndPageFromTurn(
|
||||||
|
chapter: chapter, page: page, turn: .next)
|
||||||
|
}
|
||||||
|
|
||||||
let (pc, pp) = getChapterAndPageFromTurn(
|
|
||||||
chapter: currentChapter, page: currentPage, turn: .previous)
|
|
||||||
previousImage = getImage(
|
|
||||||
chapter: pc, volume: Int(metadata.chapters[pc - 1].volume),
|
|
||||||
page: pp, path: currentPath)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -781,9 +805,8 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
|
|||||||
let dir: URL = directories[0]
|
let dir: URL = directories[0]
|
||||||
let ps: [String] = try fileManager.contentsOfDirectory(atPath: dir.path).sorted()
|
let ps: [String] = try fileManager.contentsOfDirectory(atPath: dir.path).sorted()
|
||||||
let current = dir.appendingPathComponent(ps[0])
|
let current = dir.appendingPathComponent(ps[0])
|
||||||
let next = dir.appendingPathComponent(ps[1])
|
imageView.image = UIImage(contentsOfFile: current.path)
|
||||||
currentImage = UIImage(contentsOfFile: current.path)
|
|
||||||
nextImage = UIImage(contentsOfFile: next.path)
|
|
||||||
let path_meta = current.lastPathComponent.components(separatedBy: "_")
|
let path_meta = current.lastPathComponent.components(separatedBy: "_")
|
||||||
assert(path_meta[0] == "chapter")
|
assert(path_meta[0] == "chapter")
|
||||||
currentChapter = Int(path_meta[1])!
|
currentChapter = Int(path_meta[1])!
|
||||||
@@ -888,3 +911,54 @@ class ImageCell: UICollectionViewCell {
|
|||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func preloadImage(at path: URL) {
|
||||||
|
loadImage(at: path, completion: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadImage(at path: URL, completion: ((UIImage?) -> Void)?) {
|
||||||
|
if let cached = cache.object(forKey: path.path as NSString) {
|
||||||
|
completion?(cached)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if loadingTasks[path.path] != nil {
|
||||||
|
if let completion = completion {
|
||||||
|
loadingTasks[path.path]?.append(completion)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingTasks[path.path] = completion != nil ? [completion!] : []
|
||||||
|
|
||||||
|
queue.async { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
let image = UIImage(contentsOfFile: path.path)
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let image = image, let data = image.pngData() {
|
||||||
|
self.cache.setObject(image, forKey: path.path as NSString, cost: data.count)
|
||||||
|
} else {
|
||||||
|
print("no image to cache found")
|
||||||
|
}
|
||||||
|
|
||||||
|
self.loadingTasks[path.path]?.forEach { $0(image) }
|
||||||
|
self.loadingTasks.removeValue(forKey: path.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageIfLoaded(at path: String) -> UIImage? {
|
||||||
|
return cache.object(forKey: path as NSString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user